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