my gripes with CSort (and solution)

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:

  1. CSort does not behave well when used with relational queries.

  2. 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:

  1. 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 :slight_smile:

You surely did a great job. CSort wasn’t designed for supporting relational query initially. I agree with you it is cumbersome to use with relational query. Could you create a ticket for this? Please include also the address to this post. Thanks.

Thank you :slight_smile:

I hope to contribute useful stuff in the future - I am more excited about Yii than any framework I’ve ever worked with, to the point that my own desires to write a custom framework are fading… I have been planning and experimenting for a couple of years - but almost everything I had hoped to do, I found, has already been done in Yii, almost to my own exact specifications! :wink:

I’ve come up with a very similar solution for a database front-end that I am writing. I found that I needed applyOrderTo() to return an array to pass to with() before the findAll(). I see you have withNames(), what does this do, may I ask?

I also wrote a companion class called CActiveSelection that allows the user to select columns to display in a table, and the columns can be things like "name" which is made from a firstname and lastname. It can also select, for example, the id so that the name can be hyperlinked.

Instead of the sorts() array in the model, mine is called calculatedAttributes(), since it can provide the attributes for sorting and selecting.

My code is not ready to be posted, but I would love if CActiveSort and CActiveSelection could be added together along with a few tweaks to CDbCriteria that they require.

withNames() is just a method I use to abstract a with() call that has lots of arguments, e.g.:




public function withNames() {

  return $this->with(...);

}



not really a named scope, just a simplified way to have all those join criteria in one place.