1: <?php
2: /**
3: * CSort class file.
4: *
5: * @author Qiang Xue <qiang.xue@gmail.com>
6: * @link http://www.yiiframework.com/
7: * @copyright 2008-2013 Yii Software LLC
8: * @license http://www.yiiframework.com/license/
9: */
10:
11: /**
12: * CSort represents information relevant to sorting.
13: *
14: * When data needs to be sorted according to one or several attributes,
15: * we can use CSort to represent the sorting information and generate
16: * appropriate hyperlinks that can lead to sort actions.
17: *
18: * CSort is designed to be used together with {@link CActiveRecord}.
19: * When creating a CSort instance, you need to specify {@link modelClass}.
20: * You can use CSort to generate hyperlinks by calling {@link link}.
21: * You can also use CSort to modify a {@link CDbCriteria} instance by calling {@link applyOrder} so that
22: * it can cause the query results to be sorted according to the specified
23: * attributes.
24: *
25: * In order to prevent SQL injection attacks, CSort ensures that only valid model attributes
26: * can be sorted. This is determined based on {@link modelClass} and {@link attributes}.
27: * When {@link attributes} is not set, all attributes belonging to {@link modelClass}
28: * can be sorted. When {@link attributes} is set, only those attributes declared in the property
29: * can be sorted.
30: *
31: * By configuring {@link attributes}, one can perform more complex sorts that may
32: * consist of things like compound attributes (e.g. sort based on the combination of
33: * first name and last name of users).
34: *
35: * The property {@link attributes} should be an array of key-value pairs, where the keys
36: * represent the attribute names, while the values represent the virtual attribute definitions.
37: * For more details, please check the documentation about {@link attributes}.
38: *
39: * @property string $orderBy The order-by columns represented by this sort object.
40: * This can be put in the ORDER BY clause of a SQL statement.
41: * @property array $directions Sort directions indexed by attribute names.
42: * The sort direction. Can be either CSort::SORT_ASC for ascending order or
43: * CSort::SORT_DESC for descending order.
44: *
45: * @author Qiang Xue <qiang.xue@gmail.com>
46: * @package system.web
47: */
48: class CSort extends CComponent
49: {
50: /**
51: * Sort ascending
52: * @since 1.1.10
53: */
54: const SORT_ASC = false;
55:
56: /**
57: * Sort descending
58: * @since 1.1.10
59: */
60: const SORT_DESC = true;
61:
62: /**
63: * @var boolean whether the sorting can be applied to multiple attributes simultaneously.
64: * Defaults to false, which means each time the data can only be sorted by one attribute.
65: */
66: public $multiSort=false;
67: /**
68: * @var string the name of the model class whose attributes can be sorted.
69: * The model class must be a child class of {@link CActiveRecord}.
70: */
71: public $modelClass;
72: /**
73: * @var array list of attributes that are allowed to be sorted.
74: * For example, array('user_id','create_time') would specify that only 'user_id'
75: * and 'create_time' of the model {@link modelClass} can be sorted.
76: * By default, this property is an empty array, which means all attributes in
77: * {@link modelClass} are allowed to be sorted.
78: *
79: * This property can also be used to specify complex sorting. To do so,
80: * a virtual attribute can be declared in terms of a key-value pair in the array.
81: * The key refers to the name of the virtual attribute that may appear in the sort request,
82: * while the value specifies the definition of the virtual attribute.
83: *
84: * In the simple case, a key-value pair can be like <code>'user'=>'user_id'</code>
85: * where 'user' is the name of the virtual attribute while 'user_id' means the virtual
86: * attribute is the 'user_id' attribute in the {@link modelClass}.
87: *
88: * A more flexible way is to specify the key-value pair as
89: * <pre>
90: * 'user'=>array(
91: * 'asc'=>'first_name, last_name',
92: * 'desc'=>'first_name DESC, last_name DESC',
93: * 'label'=>'Name'
94: * )
95: * </pre>
96: * where 'user' is the name of the virtual attribute that specifies the full name of user
97: * (a compound attribute consisting of first name and last name of user). In this case,
98: * we have to use an array to define the virtual attribute with three elements: 'asc',
99: * 'desc' and 'label'.
100: *
101: * The above approach can also be used to declare virtual attributes that consist of relational
102: * attributes. For example,
103: * <pre>
104: * 'price'=>array(
105: * 'asc'=>'item.price',
106: * 'desc'=>'item.price DESC',
107: * 'label'=>'Item Price'
108: * )
109: * </pre>
110: *
111: * Note, the attribute name should not contain '-' or '.' characters because
112: * they are used as {@link separators}.
113: *
114: * Starting from version 1.1.3, an additional option named 'default' can be used in the virtual attribute
115: * declaration. This option specifies whether an attribute should be sorted in ascending or descending
116: * order upon user clicking the corresponding sort hyperlink if it is not currently sorted. The valid
117: * option values include 'asc' (default) and 'desc'. For example,
118: * <pre>
119: * 'price'=>array(
120: * 'asc'=>'item.price',
121: * 'desc'=>'item.price DESC',
122: * 'label'=>'Item Price',
123: * 'default'=>'desc',
124: * )
125: * </pre>
126: *
127: * Also starting from version 1.1.3, you can include a star ('*') element in this property so that
128: * all model attributes are available for sorting, in addition to those virtual attributes. For example,
129: * <pre>
130: * 'attributes'=>array(
131: * 'price'=>array(
132: * 'asc'=>'item.price',
133: * 'desc'=>'item.price DESC',
134: * 'label'=>'Item Price',
135: * 'default'=>'desc',
136: * ),
137: * '*',
138: * )
139: * </pre>
140: * Note that when a name appears as both a model attribute and a virtual attribute, the position of
141: * the star element in the array determines which one takes precedence. In particular, if the star
142: * element is the first element in the array, the model attribute takes precedence; and if the star
143: * element is the last one, the virtual attribute takes precedence.
144: */
145: public $attributes=array();
146: /**
147: * @var string the name of the GET parameter that specifies which attributes to be sorted
148: * in which direction. Defaults to 'sort'.
149: */
150: public $sortVar='sort';
151: /**
152: * @var string the tag appeared in the GET parameter that indicates the attribute should be sorted
153: * in descending order. Defaults to 'desc'.
154: */
155: public $descTag='desc';
156: /**
157: * @var mixed the default order that should be applied to the query criteria when
158: * the current request does not specify any sort. For example, 'name, create_time DESC' or
159: * 'UPPER(name)'.
160: *
161: * Starting from version 1.1.3, you can also specify the default order using an array.
162: * The array keys could be attribute names or virtual attribute names as declared in {@link attributes},
163: * and the array values indicate whether the sorting of the corresponding attributes should
164: * be in descending order. For example,
165: * <pre>
166: * 'defaultOrder'=>array(
167: * 'price'=>CSort::SORT_DESC,
168: * )
169: * </pre>
170: * `SORT_DESC` and `SORT_ASC` are available since 1.1.10. In earlier Yii versions you should use
171: * `true` and `false` respectively.
172: *
173: * Please note when using array to specify the default order, the corresponding attributes
174: * will be put into {@link directions} and thus affect how the sort links are rendered
175: * (e.g. an arrow may be displayed next to the currently active sort link).
176: */
177: public $defaultOrder;
178: /**
179: * @var string the route (controller ID and action ID) for generating the sorted contents.
180: * Defaults to empty string, meaning using the currently requested route.
181: */
182: public $route='';
183: /**
184: * @var array separators used in the generated URL. This must be an array consisting of
185: * two elements. The first element specifies the character separating different
186: * attributes, while the second element specifies the character separating attribute name
187: * and the corresponding sort direction. Defaults to array('-','.').
188: */
189: public $separators=array('-','.');
190: /**
191: * @var array the additional GET parameters (name=>value) that should be used when generating sort URLs.
192: * Defaults to null, meaning using the currently available GET parameters.
193: */
194: public $params;
195:
196: private $_directions;
197:
198: /**
199: * Constructor.
200: * @param string $modelClass the class name of data models that need to be sorted.
201: * This should be a child class of {@link CActiveRecord}.
202: */
203: public function __construct($modelClass=null)
204: {
205: $this->modelClass=$modelClass;
206: }
207:
208: /**
209: * Modifies the query criteria by changing its {@link CDbCriteria::order} property.
210: * This method will use {@link directions} to determine which columns need to be sorted.
211: * They will be put in the ORDER BY clause. If the criteria already has non-empty {@link CDbCriteria::order} value,
212: * the new value will be appended to it.
213: * @param CDbCriteria $criteria the query criteria
214: */
215: public function applyOrder($criteria)
216: {
217: $order=$this->getOrderBy($criteria);
218: if(!empty($order))
219: {
220: if(!empty($criteria->order))
221: $criteria->order.=', ';
222: $criteria->order.=$order;
223: }
224: }
225:
226: /**
227: * @param CDbCriteria $criteria the query criteria
228: * @return string the order-by columns represented by this sort object.
229: * This can be put in the ORDER BY clause of a SQL statement.
230: * @since 1.1.0
231: */
232: public function getOrderBy($criteria=null)
233: {
234: $directions=$this->getDirections();
235: if(empty($directions))
236: return is_string($this->defaultOrder) ? $this->defaultOrder : '';
237: else
238: {
239: if($this->modelClass!==null)
240: $schema=$this->getModel($this->modelClass)->getDbConnection()->getSchema();
241: $orders=array();
242: foreach($directions as $attribute=>$descending)
243: {
244: $definition=$this->resolveAttribute($attribute);
245: if(is_array($definition))
246: {
247: if($descending)
248: $orders[]=isset($definition['desc']) ? (is_array($definition['desc']) ? implode(', ',$definition['desc']) : $definition['desc']) : $attribute.' DESC';
249: else
250: $orders[]=isset($definition['asc']) ? (is_array($definition['asc']) ? implode(', ',$definition['asc']) : $definition['asc']) : $attribute;
251: }
252: elseif($definition!==false)
253: {
254: $attribute=$definition;
255: if(isset($schema))
256: {
257: if(($pos=strpos($attribute,'.'))!==false)
258: $attribute=$schema->quoteTableName(substr($attribute,0,$pos)).'.'.$schema->quoteColumnName(substr($attribute,$pos+1));
259: else
260: $attribute=($criteria===null || $criteria->alias===null ? $this->getModel($this->modelClass)->getTableAlias(true) : $schema->quoteTableName($criteria->alias)).'.'.$schema->quoteColumnName($attribute);
261: }
262: $orders[]=$descending?$attribute.' DESC':$attribute;
263: }
264: }
265: return implode(', ',$orders);
266: }
267: }
268:
269: /**
270: * Generates a hyperlink that can be clicked to cause sorting.
271: * @param string $attribute the attribute name. This must be the actual attribute name, not alias.
272: * If it is an attribute of a related AR object, the name should be prefixed with
273: * the relation name (e.g. 'author.name', where 'author' is the relation name).
274: * @param string $label the link label. If null, the label will be determined according
275: * to the attribute (see {@link resolveLabel}).
276: * @param array $htmlOptions additional HTML attributes for the hyperlink tag
277: * @return string the generated hyperlink
278: */
279: public function link($attribute,$label=null,$htmlOptions=array())
280: {
281: if($label===null)
282: $label=$this->resolveLabel($attribute);
283: if(($definition=$this->resolveAttribute($attribute))===false)
284: return $label;
285: $directions=$this->getDirections();
286: if(isset($directions[$attribute]))
287: {
288: $class=$directions[$attribute] ? 'desc' : 'asc';
289: if(isset($htmlOptions['class']))
290: $htmlOptions['class'].=' '.$class;
291: else
292: $htmlOptions['class']=$class;
293: $descending=!$directions[$attribute];
294: unset($directions[$attribute]);
295: }
296: elseif(is_array($definition) && isset($definition['default']))
297: $descending=$definition['default']==='desc';
298: else
299: $descending=false;
300:
301: if($this->multiSort)
302: $directions=array_merge(array($attribute=>$descending),$directions);
303: else
304: $directions=array($attribute=>$descending);
305:
306: $url=$this->createUrl(Yii::app()->getController(),$directions);
307:
308: return $this->createLink($attribute,$label,$url,$htmlOptions);
309: }
310:
311: /**
312: * Resolves the attribute label for the specified attribute.
313: * This will invoke {@link CActiveRecord::getAttributeLabel} to determine what label to use.
314: * If the attribute refers to a virtual attribute declared in {@link attributes},
315: * then the label given in the {@link attributes} will be returned instead.
316: * @param string $attribute the attribute name.
317: * @return string the attribute label
318: */
319: public function resolveLabel($attribute)
320: {
321: $definition=$this->resolveAttribute($attribute);
322: if(is_array($definition))
323: {
324: if(isset($definition['label']))
325: return $definition['label'];
326: }
327: elseif(is_string($definition))
328: $attribute=$definition;
329: if($this->modelClass!==null)
330: return $this->getModel($this->modelClass)->getAttributeLabel($attribute);
331: else
332: return $attribute;
333: }
334:
335: /**
336: * Returns the currently requested sort information.
337: * @return array sort directions indexed by attribute names.
338: * Sort direction can be either CSort::SORT_ASC for ascending order or
339: * CSort::SORT_DESC for descending order.
340: */
341: public function getDirections()
342: {
343: if($this->_directions===null)
344: {
345: $this->_directions=array();
346: if(isset($_GET[$this->sortVar]) && is_string($_GET[$this->sortVar]))
347: {
348: $attributes=explode($this->separators[0],$_GET[$this->sortVar]);
349: foreach($attributes as $attribute)
350: {
351: if(($pos=strrpos($attribute,$this->separators[1]))!==false)
352: {
353: $descending=substr($attribute,$pos+1)===$this->descTag;
354: if($descending)
355: $attribute=substr($attribute,0,$pos);
356: }
357: else
358: $descending=false;
359:
360: if(($this->resolveAttribute($attribute))!==false)
361: {
362: $this->_directions[$attribute]=$descending;
363: if(!$this->multiSort)
364: return $this->_directions;
365: }
366: }
367: }
368: if($this->_directions===array() && is_array($this->defaultOrder))
369: $this->_directions=$this->defaultOrder;
370: }
371: return $this->_directions;
372: }
373:
374: /**
375: * Returns the sort direction of the specified attribute in the current request.
376: * @param string $attribute the attribute name
377: * @return mixed Sort direction of the attribute. Can be either CSort::SORT_ASC
378: * for ascending order or CSort::SORT_DESC for descending order. Value is null
379: * if the attribute doesn't need to be sorted.
380: */
381: public function getDirection($attribute)
382: {
383: $this->getDirections();
384: return isset($this->_directions[$attribute]) ? $this->_directions[$attribute] : null;
385: }
386:
387: /**
388: * Creates a URL that can lead to generating sorted data.
389: * @param CController $controller the controller that will be used to create the URL.
390: * @param array $directions the sort directions indexed by attribute names.
391: * The sort direction can be either CSort::SORT_ASC for ascending order or
392: * CSort::SORT_DESC for descending order.
393: * @return string the URL for sorting
394: */
395: public function createUrl($controller,$directions)
396: {
397: $sorts=array();
398: foreach($directions as $attribute=>$descending)
399: $sorts[]=$descending ? $attribute.$this->separators[1].$this->descTag : $attribute;
400: $params=$this->params===null ? $_GET : $this->params;
401: $params[$this->sortVar]=implode($this->separators[0],$sorts);
402: return $controller->createUrl($this->route,$params);
403: }
404:
405: /**
406: * Returns the real definition of an attribute given its name.
407: *
408: * The resolution is based on {@link attributes} and {@link CActiveRecord::attributeNames}.
409: * <ul>
410: * <li>When {@link attributes} is an empty array, if the name refers to an attribute of {@link modelClass},
411: * then the name is returned back.</li>
412: * <li>When {@link attributes} is not empty, if the name refers to an attribute declared in {@link attributes},
413: * then the corresponding virtual attribute definition is returned. Starting from version 1.1.3, if {@link attributes}
414: * contains a star ('*') element, the name will also be used to match against all model attributes.</li>
415: * <li>In all other cases, false is returned, meaning the name does not refer to a valid attribute.</li>
416: * </ul>
417: * @param string $attribute the attribute name that the user requests to sort on
418: * @return mixed the attribute name or the virtual attribute definition. False if the attribute cannot be sorted.
419: */
420: public function resolveAttribute($attribute)
421: {
422: if($this->attributes!==array())
423: $attributes=$this->attributes;
424: elseif($this->modelClass!==null)
425: $attributes=$this->getModel($this->modelClass)->attributeNames();
426: else
427: return false;
428: foreach($attributes as $name=>$definition)
429: {
430: if(is_string($name))
431: {
432: if($name===$attribute)
433: return $definition;
434: }
435: elseif($definition==='*')
436: {
437: if($this->modelClass!==null && $this->getModel($this->modelClass)->hasAttribute($attribute))
438: return $attribute;
439: }
440: elseif($definition===$attribute)
441: return $attribute;
442: }
443: return false;
444: }
445:
446: /**
447: * Given active record class name returns new model instance.
448: *
449: * @param string $className active record class name.
450: * @return CActiveRecord active record model instance.
451: *
452: * @since 1.1.14
453: */
454: protected function getModel($className)
455: {
456: return CActiveRecord::model($className);
457: }
458:
459: /**
460: * Creates a hyperlink based on the given label and URL.
461: * You may override this method to customize the link generation.
462: * @param string $attribute the name of the attribute that this link is for
463: * @param string $label the label of the hyperlink
464: * @param string $url the URL
465: * @param array $htmlOptions additional HTML options
466: * @return string the generated hyperlink
467: */
468: protected function createLink($attribute,$label,$url,$htmlOptions)
469: {
470: return CHtml::link($label,$url,$htmlOptions);
471: }
472: }
473: