CSort.php 17.1 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 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 139 140 141 142 143 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 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 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 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460
<?php
/**
 * CSort class file.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
 * @copyright Copyright &copy; 2008-2011 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

/**
 * CSort represents information relevant to sorting.
 *
 * When data needs to be sorted according to one or several attributes,
 * we can use CSort to represent the sorting information and generate
 * appropriate hyperlinks that can lead to sort actions.
 *
 * CSort is designed to be used together with {@link CActiveRecord}.
 * When creating a CSort instance, you need to specify {@link modelClass}.
 * You can use CSort to generate hyperlinks by calling {@link link}.
 * You can also use CSort 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, CSort 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}.
 *
 * @property string $orderBy The order-by columns represented by this sort object.
 * This can be put in the ORDER BY clause of a SQL statement.
 * @property array $directions Sort directions indexed by attribute names.
 * The sort direction. Can be either CSort::SORT_ASC for ascending order or
 * CSort::SORT_DESC for descending order.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id$
 * @package system.web
 */
class CSort extends CComponent
{
	/**
	 * Sort ascending
	 * @since 1.1.10
	 */
	const SORT_ASC = false;

	/**
	 * Sort descending
	 * @since 1.1.10
	 */
	const SORT_DESC = true;

	/**
	 * @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 $multiSort=false;
	/**
	 * @var string the name of the model class whose attributes can be sorted.
	 * The model class must be a child class of {@link CActiveRecord}.
	 */
	public $modelClass;
	/**
	 * @var array list of attributes that are allowed to be sorted.
	 * For example, array('user_id','create_time') would specify that only 'user_id'
	 * 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
	 * attribute is the 'user_id' attribute in the {@link modelClass}.
	 *
	 * A more flexible way is to specify the key-value pair as
	 * <pre>
	 * 'user'=>array(
	 *     '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
	 * they are used as {@link separators}.
	 *
	 * 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
	 * order upon user clicking the corresponding sort hyperlink if it is not currently sorted. The valid
	 * option values include 'asc' (default) and 'desc'. For example,
	 * <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
	 * all model attributes are available for sorting, in addition to those virtual attributes. For example,
	 * <pre>
	 * 'attributes'=>array(
	 *     'price'=>array(
	 *         'asc'=>'item.price',
	 *         'desc'=>'item.price DESC',
	 *         'label'=>'Item Price',
	 *         'default'=>'desc',
	 *     ),
	 *     '*',
	 * )
	 * </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();
	/**
	 * @var string the name of the GET parameter that specifies which attributes to be sorted
	 * in which direction. Defaults to 'sort'.
	 */
	public $sortVar='sort';
	/**
	 * @var string the tag appeared in the GET parameter that indicates the attribute should be sorted
	 * in descending order. Defaults to 'desc'.
	 */
	public $descTag='desc';
	/**
	 * @var mixed the default order that should be applied to the query criteria when
	 * the current request does not specify any sort. For example, 'name, create_time DESC' or
	 * '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},
	 * and the array values indicate whether the sorting of the corresponding attributes should
	 * be in descending order. For example,
	 * <pre>
	 * 'defaultOrder'=>array(
	 *     'price'=>CSort::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
	 * 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;
	/**
	 * @var string the route (controller ID and action ID) for generating the sorted contents.
	 * Defaults to empty string, meaning using the currently requested route.
	 */
	public $route='';
	/**
	 * @var array separators used in the generated URL. This must be an array consisting of
	 * two elements. The first element specifies the character separating different
	 * attributes, while the second element specifies the character separating attribute name
	 * and the corresponding sort direction. Defaults to array('-','.').
	 */
	public $separators=array('-','.');
	/**
	 * @var array the additional GET parameters (name=>value) that should be used when generating sort URLs.
	 * Defaults to null, meaning using the currently available GET parameters.
	 */
	public $params;

	private $_directions;

	/**
	 * Constructor.
	 * @param string $modelClass the class name of data models that need to be sorted.
	 * This should be a child class of {@link CActiveRecord}.
	 */
	public function __construct($modelClass=null)
	{
		$this->modelClass=$modelClass;
	}

	/**
	 * Modifies the query criteria by changing its {@link CDbCriteria::order} property.
	 * This method will use {@link directions} to determine which columns need to be sorted.
	 * They will be put in the ORDER BY clause. If the criteria already has non-empty {@link CDbCriteria::order} value,
	 * the new value will be appended to it.
	 * @param CDbCriteria $criteria the query criteria
	 */
	public function applyOrder($criteria)
	{
		$order=$this->getOrderBy($criteria);
		if(!empty($order))
		{
			if(!empty($criteria->order))
				$criteria->order.=', ';
			$criteria->order.=$order;
		}
	}

	/**
	 * @param CDbCriteria $criteria the query criteria
	 * @return string the order-by columns represented by this sort object.
	 * This can be put in the ORDER BY clause of a SQL statement.
	 * @since 1.1.0
	 */
	public function getOrderBy($criteria=null)
	{
		$directions=$this->getDirections();
		if(empty($directions))
			return is_string($this->defaultOrder) ? $this->defaultOrder : '';
		else
		{
			if($this->modelClass!==null)
				$schema=CActiveRecord::model($this->modelClass)->getDbConnection()->getSchema();
			$orders=array();
			foreach($directions as $attribute=>$descending)
			{
				$definition=$this->resolveAttribute($attribute);
				if(is_array($definition))
				{
					if($descending)
						$orders[]=isset($definition['desc']) ? $definition['desc'] : $attribute.' DESC';
					else
						$orders[]=isset($definition['asc']) ? $definition['asc'] : $attribute;
				}
				else if($definition!==false)
				{
					$attribute=$definition;
					if(isset($schema))
					{
						if(($pos=strpos($attribute,'.'))!==false)
							$attribute=$schema->quoteTableName(substr($attribute,0,$pos)).'.'.$schema->quoteColumnName(substr($attribute,$pos+1));
						else
							$attribute=($criteria===null || $criteria->alias===null ? CActiveRecord::model($this->modelClass)->getTableAlias(true) : $criteria->alias).'.'.$schema->quoteColumnName($attribute);
					}
					$orders[]=$descending?$attribute.' DESC':$attribute;
				}
			}
			return implode(', ',$orders);
		}
	}

	/**
	 * Generates a hyperlink that can be clicked to cause sorting.
	 * @param string $attribute the attribute name. This must be the actual attribute name, not alias.
	 * If it is an attribute of a related AR object, the name should be prefixed with
	 * the relation name (e.g. 'author.name', where 'author' is the relation name).
	 * @param string $label the link label. If null, the label will be determined according
	 * to the attribute (see {@link resolveLabel}).
	 * @param array $htmlOptions additional HTML attributes for the hyperlink tag
	 * @return string the generated hyperlink
	 */
	public function link($attribute,$label=null,$htmlOptions=array())
	{
		if($label===null)
			$label=$this->resolveLabel($attribute);
		if(($definition=$this->resolveAttribute($attribute))===false)
			return $label;
		$directions=$this->getDirections();
		if(isset($directions[$attribute]))
		{
			$class=$directions[$attribute] ? 'desc' : 'asc';
			if(isset($htmlOptions['class']))
				$htmlOptions['class'].=' '.$class;
			else
				$htmlOptions['class']=$class;
			$descending=!$directions[$attribute];
			unset($directions[$attribute]);
		}
		else if(is_array($definition) && isset($definition['default']))
			$descending=$definition['default']==='desc';
		else
			$descending=false;

		if($this->multiSort)
			$directions=array_merge(array($attribute=>$descending),$directions);
		else
			$directions=array($attribute=>$descending);

		$url=$this->createUrl(Yii::app()->getController(),$directions);

		return $this->createLink($attribute,$label,$url,$htmlOptions);
	}

	/**
	 * Resolves the attribute label for the specified attribute.
	 * This will invoke {@link CActiveRecord::getAttributeLabel} to determine what label to use.
	 * If the attribute refers to a virtual attribute declared in {@link attributes},
	 * then the label given in the {@link attributes} will be returned instead.
	 * @param string $attribute the attribute name.
	 * @return string the attribute label
	 */
	public function resolveLabel($attribute)
	{
		$definition=$this->resolveAttribute($attribute);
		if(is_array($definition))
		{
			if(isset($definition['label']))
				return $definition['label'];
		}
		else if(is_string($definition))
			$attribute=$definition;
		if($this->modelClass!==null)
			return CActiveRecord::model($this->modelClass)->getAttributeLabel($attribute);
		else
			return $attribute;
	}

	/**
	 * Returns the currently requested sort information.
	 * @return array sort directions indexed by attribute names.
	 * Sort direction can be either CSort::SORT_ASC for ascending order or
	 * CSort::SORT_DESC for descending order.
	 */
	public function getDirections()
	{
		if($this->_directions===null)
		{
			$this->_directions=array();
			if(isset($_GET[$this->sortVar]) && is_string($_GET[$this->sortVar]))
			{
				$attributes=explode($this->separators[0],$_GET[$this->sortVar]);
				foreach($attributes as $attribute)
				{
					if(($pos=strrpos($attribute,$this->separators[1]))!==false)
					{
						$descending=substr($attribute,$pos+1)===$this->descTag;
						if($descending)
							$attribute=substr($attribute,0,$pos);
					}
					else
						$descending=false;

					if(($this->resolveAttribute($attribute))!==false)
					{
						$this->_directions[$attribute]=$descending;
						if(!$this->multiSort)
							return $this->_directions;
					}
				}
			}
			if($this->_directions===array() && is_array($this->defaultOrder))
				$this->_directions=$this->defaultOrder;
		}
		return $this->_directions;
	}

	/**
	 * Returns the sort direction of the specified attribute in the current request.
	 * @param string $attribute the attribute name
	 * @return mixed Sort direction of the attribute. Can be either CSort::SORT_ASC
	 * for ascending order or CSort::SORT_DESC for descending order. Value is null
	 * if the attribute doesn't need to be sorted.
	 */
	public function getDirection($attribute)
	{
		$this->getDirections();
		return isset($this->_directions[$attribute]) ? $this->_directions[$attribute] : null;
	}

	/**
	 * Creates a URL that can lead to generating sorted data.
	 * @param CController $controller the controller that will be used to create the URL.
	 * @param array $directions the sort directions indexed by attribute names.
	 * The sort direction can be either CSort::SORT_ASC for ascending order or
	 * CSort::SORT_DESC for descending order.
	 * @return string the URL for sorting
	 */
	public function createUrl($controller,$directions)
	{
		$sorts=array();
		foreach($directions as $attribute=>$descending)
			$sorts[]=$descending ? $attribute.$this->separators[1].$this->descTag : $attribute;
		$params=$this->params===null ? $_GET : $this->params;
		$params[$this->sortVar]=implode($this->separators[0],$sorts);
		return $controller->createUrl($this->route,$params);
	}

	/**
	 * Returns the real definition of an attribute given its name.
	 *
	 * The resolution is based on {@link attributes} and {@link CActiveRecord::attributeNames}.
	 * <ul>
	 * <li>When {@link attributes} is an empty array, if the name refers to an attribute of {@link modelClass},
	 * 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 resolveAttribute($attribute)
	{
		if($this->attributes!==array())
			$attributes=$this->attributes;
		else if($this->modelClass!==null)
			$attributes=CActiveRecord::model($this->modelClass)->attributeNames();
		else
			return false;
		foreach($attributes as $name=>$definition)
		{
			if(is_string($name))
			{
				if($name===$attribute)
					return $definition;
			}
			else if($definition==='*')
			{
				if($this->modelClass!==null && CActiveRecord::model($this->modelClass)->hasAttribute($attribute))
					return $attribute;
			}
			else if($definition===$attribute)
				return $attribute;
		}
		return false;
	}

	/**
	 * Creates a hyperlink based on the given label and URL.
	 * You may override this method to customize the link generation.
	 * @param string $attribute the name of the attribute that this link is for
	 * @param string $label the label of the hyperlink
	 * @param string $url the URL
	 * @param array $htmlOptions additional HTML options
	 * @return string the generated hyperlink
	 */
	protected function createLink($attribute,$label,$url,$htmlOptions)
	{
		return CHtml::link($label,$url,$htmlOptions);
	}
}