[Solved] Sort by related AR's attribute?

I have two tables customer and project (project BELONGS_TO customer). For the project’s list page, i also fetch the related customer data:

$projectList=Project::model->with('customer')->findAll($criteria);

[code=view]

<th><?php echo $sort->link( ??? ); ?></th>

[…]

<td><?php echo CHtml::encode($model->customer->name); ?></td>[/code]

Can i also use CSort to sort by an attribute of a related AR? What argument would CSort::link() need then?

I also found that i can’t sort by name anymore as this field appears in both, customer and project table:

Quote

CDbCommand konnte das SQL-Statement nicht ausführen: SQLSTATE[23000]: Integrity constraint violation: 1052 Column 'name' in order clause is ambiguous

Can CSort::link() be modified to disambiguate columns from related record with the same name? How about:

$sort->link('name')


$sort->link('customer.name')

The first always references attributes in the main table, the second one specifies an attribute from a related record.

CSort was not originally designed to work with RAR.

I think you can still give it a try, however.

You will need to specify its attributes property to set up a mapping between attribute aliases and attribute names (with prefix if necessary).

When calling link(), you need to provide a second parameter specifying the label because CSort won't be able to deduce the label based on a RAR attribute.

Thanks, Qiang.

I tried now this way:



        // In controller's list action:


        $sort=new CSort('Project');


        $sort->separators=array(':','-');


        $sort->attributes=array(


            'Project.id'=>'id',


            'Project.name'=>'name',


            'Customer.name'=>'customer'


        );


    <th><?php echo $sort->link('Project.id','Id'); ?></th>


    <th><?php echo $sort->link('Customer.name','Customer'); ?></th>


    <th><?php echo $sort->link('Project.name','Project'); ?></th>


Columns Id and Project are sortable but i need the Project. prefix in the link call, otherwhise it creates strange URL parameters like sort=id:id when clicking a second time.

But it still doesn’t work with Customer column. I get this:

Quote

CDbCommand konnte das SQL-Statement nicht ausführen: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Customer.name' in 'order clause'

Try replacing Customer.name with "??.name". Other code seems fine.

Nope, doesn't work. I tried three combinations: changing to ?? in controller only, in view only or in both, controller and view. The first two doesn't do anything. When changing it in both i get:

Quote

CDbCommand konnte das SQL-Statement nicht ausführen: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '??.name LIMIT 10' at line 1

hmm…could you log the SQL statements being executed? I want to see what is happening here.

Ok, the original (german) code looks like this (Projekt==Project, Kunde==Customer):

        $sort->attributes=array(


        $sort->attributes=array(


            'Projekt.id'=>'id',


            'Projekt.name'=>'name',


            '??.name'=>'kunde',


            'Projekt.nummer'=>'nummer',


            'Projekt.changedate'=>'change',


            'Projekt.createdate'=>'create'


        );


    <th><?php echo $sort->link('??.name','Kunde'); ?></th>


Column header has &sort=kunde as URL parameter. When clicking on Column Kunde (Customer), the SQL is:

 SELECT `Projekt`.`id` AS t0_c0, `Projekt`.`kundeID` AS t0_c1, `Projekt`.`name` AS t0_c2, `Projekt`.`nummer` AS t0_c3, `Projekt`.`changedate` AS t0_c4, `Projekt`.`createdate` AS t0_c5, t1.`id` AS t1_c0, t1.`nummer` AS t1_c1, t1.`name` AS t1_c2, t1.`changedate` AS t1_c3, t1.`createdate` AS t1_c4 FROM `Projekt`  LEFT OUTER JOIN `Kunde` t1 ON `Projekt`.`kundeID`=t1.`id` ORDER BY ??.name LIMIT 10


I see the reason.

In your Project class, when you specify 'customer' relation, set the 'alias' option to be 'Customer' (otherwise AR will generate automatically a table alias). Then change all "??" reference to "Customer".

Thanks Qiang, it works! :)

I have a similar scenario, where the created field is in two table resulting in an error.  I was however able to fix it with this code:

<?php


//controller


$sort = new CSort('User');


$sort->attributes = array(


	'user.username'=>'username',


	'user.group_id'=>'group_id',


	'user.email'=>'email',


	'user.created'=>'created',


	'user.email_confirmed'=>'email_confirmed',


);


$sort->applyOrder($criteria);








//view


?>


<th><?php echo $sort->link('user.username'); ?></th>


<th><?php echo $sort->link('user.group_id'); ?></th>


<th><?php echo $sort->link('user.email'); ?></th>


<th><?php echo $sort->link('user.created'); ?></th>


<th><?php echo $sort->link('user.email_confirmed'); ?></th>


Now however the aliases are like 'user.username' instead of what is defined in user model attributeLabels() method.  Is it possible to force it to use attributeLabels()?

The link method should use the alias you defined in $sort-attributes. I think attributeLabels() is for a different purpose and is not appropriate to be used as sort parameter names.

So it should be like this?:

<?php


<th><?php echo $sort->link('username'); ?></th>


<th><?php echo $sort->link('group_id'); ?></th>


<th><?php echo $sort->link('email'); ?></th>


<th><?php echo $sort->link('created'); ?></th>


<th><?php echo $sort->link('email_confirmed'); ?></th>


When I do that, the labels display correctly (it actually does take into account attributeLabels() believe it or not, see line 134 of CSort).

But now it does not properly sort descending the second time I press the sort button, but instead generates something of the following:

user/list/sort/email.email

I think it should be

user/list/sort/email-desc or something like that.

Not sure what's happening here…Maybe you can give me an example that can reproduce the issue?

Ok I believe this is all the relevant code:

<?php


//controller list action


public function actionList() {


	$criteria = new CDbCriteria;





	$pages = new CPagination(User::model()->count());


	$pages->pageSize = 25;


	$pages->applyLimit($criteria);


	


	$sort = new CSort('User');


	$sort->attributes = array(


		'user.username'=>'username',


		'user.group_id'=>'group_id',


		'user.email'=>'email',


		'user.created'=>'created',


		'user.email_confirmed'=>'email_confirmed',


	);


	$sort->applyOrder($criteria);


	


	$users=User::model()->with('group')->findAll($criteria);


	


	//This is for ajax, but I turned JS off before testing it.  So this is not the problem


	if (Yii::app()->request->isAjaxRequest)


		$this->renderPartial('listPage', compact('users', 'pages', 'sort'));


	else


		$this->render('list', compact('users', 'pages', 'sort'));


}


//attributeLabels()


	public function attributeLabels() {


return array(


		'group_id'=>'User Level',


		'email_confirmed'=>'Activated?',


		'created'=>'Date Registered',


		'email_visible'=>'Show Email',


	);


}





//view code that is relevant


<th><?php echo $sort->link('username'); ?></th>


<th><?php echo $sort->link('group_id'); ?></th>


<th><?php echo $sort->link('email'); ?></th>


<th><?php echo $sort->link('created'); ?></th>


<th><?php echo $sort->link('email_confirmed'); ?></th>


What's happening:

The aliases are correctly pulled from attributeLabels() for the column headers.

When I click a header (for example username), it correctly sorts by it by generating the following url:

http://localhost/use…t/sort/username

When I click the username column header again, it should sort by it again but descending instead.  But it does not work correctly, and generates the following url:

http://localhost/use…ername.username

I also had this problem and i think, this solved it:

<?php


$sort->separators=array(':','-');

Not working either.  I will try to make a downloadable example to investigate further.

I removed the pagination code to, and it still doesn't work, so FYI it's not that either.

The root of the problem seems to be here:

<?php


	public function link($attribute,$label=null,$htmlOptions=array())


	{


		$directions=$this->getDirections();


		if(isset($directions[$attribute]))


		{


			$descending=!$directions[$attribute];


			unset($directions[$attribute]);


		}


In my instance it was looking to see if the 'username' key (the alias) was set in the $directions array, when it should have been looking for 'user.username'.  It does not seem to be taking the aliases into account.

Something like this should fix it:

<?php


	public function link($attribute,$label=null,$htmlOptions=array())


	{


		$aliases = array_flip($this->attributes);


		if (isset($aliases[$attribute]))


			$attribute = $aliases[$attribute];





		$directions=$this->getDirections();


		if(isset($directions[$attribute]))


		{


			$descending=!$directions[$attribute];


			unset($directions[$attribute]);


		}


Just to confirm: It only works, if i create the column headers like

<th><?php echo $sort->link('customer.name','Customer'); ?></th>

That works for me to, but i'm hoping that it could also work like this:



<?php echo $sort->link('name'); ?>

where the argument is the alias

   $sort->attributes = array(


      'user.name'=>'name',


   );

And it would pull the label from the model's attributeLabels()