After wasting a lot of time trying to get CSort to do what I wanted, I finally backed up and wrote my own class, for a couple of reasons:
-
CSort does not behave well when used with relational queries.
-
CSort does not allow you to create complex sorts (for example, when you have a user table with first_name and last_name, and wish to sort by both using a single alias)
and perhaps most importantly:
- Sorting logic does not belong in the controller - it should be embedded in the model.
Note that, unlike CSort, this class does not support sorting by multiple columns/aliases at once - this just complicates things unnecessarily, in my opinion, I wouldn’t even know how to build a good user interface for that, and it’s not a feature I need.
Here’s what I came up with:
<?php
class CActiveSort extends CComponent {
public $separator = '-';
public $paramName = 'sort';
public $route = '';
protected $aliases;
protected $model;
protected $current, $current_ascend = true;
public function __construct($modelName) {
$this->model = CActiveRecord::model($modelName);
$this->aliases = is_callable(array($this->model, 'sorts')) ? $this->model->sorts() : array();
if (isset($_GET['sort'])) {
$bits = explode($this->separator, $_GET['sort']);
if ($this->getAlias($bits[0])) $this->current = $bits[0];
if (isset($bits[1]) && $bits[1] == 'desc') $this->current_ascend = false;
}
}
protected function getAlias($alias) {
if (!isset($this->aliases[$alias])) {
$attributes = $this->model->attributeNames();
if (in_array($alias, $attributes)) {
$this->aliases[$alias] = $alias;
} else {
return null;
}
}
$config = $this->aliases[$alias];
if (is_string($config)) {
$this->aliases[$alias] = array(
'asc'=>$config.' asc',
'desc'=>$config.' desc'
);
}
return $config;
}
public function applyOrderTo($criteria) {
$config = $this->getAlias($this->current);
if (!$config) return;
$criteria->order = $config[$this->current_ascend ? 'asc' : 'desc'];
}
public function link($alias, $label=null, $htmlOptions=array()) {
if (is_null($label))
$label = $this->model->getAttributeLabel($alias);
$config = $this->getAlias($alias);
if (!$config)
return CHtml::encode($label); # non-sortable
$controller = Yii::app()->getController();
$ascend = ( $alias != $this->current ? false : $this->current_ascend );
$params = $_GET;
$params[$this->paramName] = $alias . $this->separator . ($ascend ? 'desc' : 'asc');
$url = $controller->createUrl($this->route, $params);
$class = $alias == $this->current ? ($ascend ? 'sort-asc' : 'sort-desc') : 'sort-none';
$htmlOptions['class'] = (isset($htmlOptions['class']) ? $htmlOptions['class'].' ' : '') . $class;
return CHtml::link($label, $url, $htmlOptions);
}
}
Usage (in the controller) becomes much simpler than with CSort, and you don’t put the sorting logic there, so:
<?php
class UserlogController extends CController
{
const PAGE_SIZE=50;
/**
* @var string specifies the default action to be 'list'.
*/
public $defaultAction='browse';
/**
* @var CActiveRecord the currently loaded data model instance.
*/
private $_model;
/**
* Manages all models.
*/
public function actionBrowse()
{
$criteria=new CDbCriteria;
$pages=new CPagination(UserLog::model()->count($criteria));
$pages->pageSize=self::PAGE_SIZE;
$pages->applyLimit($criteria);
$sort = new CActiveSort('UserLog');
$sort->applyOrderTo($criteria);
$models = UserLog::model()->withNames()->findAll($criteria);
$this->render('browse',array(
'models'=>$models,
'pages'=>$pages,
'sort'=>$sort,
));
}
}
Usage in the view is similar to CSort:
<h1>Browse Logs</h1>
<table class="dataGrid">
<thead>
<tr>
<th><?php echo $sort->link('id', 'ID'); ?></th>
<th><?php echo $sort->link('entry_type'); ?></th>
<th><?php echo $sort->link('client','Client'); ?></th>
<th><?php echo $sort->link('user','User Name'); ?></th>
<th><?php echo $sort->link('text'); ?></th>
<th><?php echo $sort->link('logged'); ?></th>
</tr>
</thead>
<tbody>
...
And now here’s the fun part - you can configure sorting aliases in your model:
<?php
class UserLog extends CActiveRecord
{
...
/**
* @return array sorting aliases for use with CActiveSort
*/
public function sorts() {
return array(
'user' => array('asc'=>'user.first_name asc, user.last_name asc', 'desc'=>'user.first_name desc, user.last_name desc'),
'client' => 'client.name',
'id' => 'userlog.id'
);
}
}
This example shows how to create a complex sort (for user), as well as simple sorts for columns in related tables. Simple sorts (strings) automatically have ‘asc’ and ‘desc’ suffixes added, while complex sorts (arrays) have to define the ‘asc’ and ‘desc’ separately - this enables you to write really complex sorts, e.g. using COALESCE() on multiple columns, etc.
In my opinion, this approach is more in tune with Yii in general.
But let me know what you think