CGridView computed column sort

My CGridView looks as follows:




$this->widget('zii.widgets.grid.CGridView', array(

    'dataProvider' => $dataProvider,

    'columns' => array(

        'customer',

        array(

            'header' => 'AOIT',

            'value' => '$data->aoit',

        ),

        array(

            'header' => 'NextAct.',

            'value' => '$data->next_activity',

        ),

        array(

            'class' => 'CButtonColumn',

        ),

    ),

));



where


$data->aoit

is a computed value from Model attributes:




public function getAoit()

{

    return 12 * $this->mrc + $this->otc;

}



I want this column to be sortable. I suppose it’s not possible with CSort since it’s not related with DB.

I was thinking about fetching db data to some Collection and do custom sorting on it. Afterwards using CGridView.

Is that possible ? How to do it ?

You can do all with mysql.

Change the dataprovider, add:


$criteria->select='*, 12*mrc+otc as aoit'

Add a public property in the model for receive the value:


public $aoit

And the trick is done. This is now a normal column in database, you can sort as usual.

Hi zaccaria,

I don’t know if this is a similar thing but in my User model I have a function that works out the user’s AGE based on their date_of_birth. I display the viryual attribute ‘age’ in my CGridView but it is not sortable. I added it in my CActiveDataProvider as follows:


'attributes'=>array(

	'age'=>array(

		'asc'=>'age',

		'desc'=>'age DESC',

		'label'=>'Age',

	),

	'*',

),

And in my CGridView:


array(

	'name'=>'age',

	'value'=>'$data->Age',

),

Do I need to perform the age calculation in MySQL, if so how?

I finally came up with using CArrayDataProvider with raw array data fetched from DB.

That makes data sorting and manipulation more flexible afterwards comparing to getting all stuff up front by SQL.

So in the Model:




public function getAllProjects(CDbCriteria $criteria)

{

    $data = array();

    $tmp = array();

	

	// fetch required data

    $result = $this->findAll($criteria); 

    // loop through array results, calculate & add business parameters 

    for ($i = 0; $i < count($result); $i++) { 

        $tmp = $result[$i]->attributes;

        $tmp['aoit'] = Helpers::getAoit($tmp['mrc'], $tmp['otc']);

        $tmp['aoit_weighted'] = Helpers::getAoitWeighted($tmp['mrc'], $tmp['otc'], $this->getStatusValue($tmp['status']));

        $data[] = $tmp;

    }

    return $data;

}



This way I can keep my computations in Model and Helpers for reuse, coping with changing business rules and staying DRY.

In the Controller:




public function actionIndex()

{

	$criteria = new CDbCriteria;

	

	$sort = new CSort;

	$sort->defaultOrder = 'aoit DESC';  // sorted column and order

	$sort->attributes = array('aoit', 'aoit_weighted'); // select sortable columns in CGridView. It's buggy which I have already reported: [url="http://www.yiiframework.com/forum/index.php?/topic/14020-cgridview-with-carraydataprovider-sorting/"]Bug Report[/url]

	

	/* NOT SHOWN: Handle here $_POST request: change sorting, criteria etc. */

	

	$rawData = Project::model()->getAllProjects($criteria);

	$dataProvider = new CArrayDataProvider($rawData, array('id' => 'projects', 'sort' => $sort));

	$this->render('index', array('dataProvider' => $dataProvider));

}




View is rather standard (just note the comments):




$this->widget('zii.widgets.grid.CGridView', array(

    'dataProvider' => $dataProvider,

    'columns' => array(

        'aoit',

        

        // Sorting-by-click will not work here as of Yii 1.1.5

        array( 

            'header' => 'AOIT x %',

            'value' => 'Helpers::formatCurrency($data["aoit_weighted"])',

        ),

        

        // Button URL parameters must be specified explicitly as of Yii 1.1.5

        array(

            'class' => 'CButtonColumn',

            'viewButtonOptions' => array('style' => 'display:none;'),

            'updateButtonUrl' => 'Yii::app()->createUrl("/project/update", array("id" => $data["id"]))',

            'deleteButtonUrl' => 'Yii::app()->createUrl("/project/delete", array("id" => $data["id"]))',

        ),

    ),

));



I hope that helps somebody. Comments are welcomed.

BTW. Yii is great framework. Keep the good job.

You could just select the atoi filed from the database and keep using CActiveDataProvider.

This allows you to save the computational time spent by sorting in php.

Just immagine, what happens if you have 20.000 records? It will not work because you are selecting 20.000 records, and if you don’t select all records, how will work the sort?

With CDbCriteria you can fire the query you want, there is no needs of a post process.

zaccaria,

I am aware of overhead that this solution creates.

In my case the module i am writing will be running on really fast machines (company servers).

I already did try that with 1000s records and it works.

However I am still eager to work with something more efficient -> CActiveDataProvider, but:

AOIT computation was easy.

Now how to get to work something like that with SQL:




aoit_weighted = aoit * probability

or the same


aoit_weighted = (12 * mrc + otc) * probability

this time ‘probability’ is not a direct db table column but value calculated by method in my Model

(calculation based on probability_id from db table).

Is there a way to make SQL calculate value in such case ? Keep in my that all calculations & business logic concerning

probability must stay in the Model, not in DB (i am using SQL Server anyway).

This is just an example. I have more like that and more will be coming once SRS Requirements change.

Thnx.

What means that is a stuff that you calculate in the model?

If it depends from field of the model, you can do as you did for atoi, if is something calcolated once and used for the query, you can do like:




$probability= ... calculate probability ...

$criteria->select= "*, 12*mrc+otc as aoit,  (12 * mrc + otc) * $probability";



In this specific case, as probability is a number between 0 and 1 it will not change the order, so you can avoid to select with db and simply order for atoi.

I strongly belive that SQL is a language with the same esperssivity of a programming language, what you can do with php you can do also with sql.

The question is who should so what: sql is better suited for search values, sort, php is better suited for presentation. Make an effort for use sql at his full power, you will be complensated with the possibility to use paging and reducing the active record created.

If you want to use CArrayDataProvdier, is better to use DAO, active record are expensive in term of memory usage.

And, last but not the least, having a powerfull machine is not a good reason for have lot of overhead :D

About business login in active record: configuring the query in active recrod is exactly the implementation of this logic, we are not writing any sql/business code outside of AR.

The difference is that instead of writing function in models, we are writing sql code for the select, but in fact is the same concept.

I try to do this, but it doesn’t work. The calculated column appear in CGridView, but I cannot sort and filter it.

Here what I do in my model:




$criteria=new CDbCriteria;

$criteria->select='*, cost*qty as amount';

$criteria->compare('bid_id',$this->bid_id);

$criteria->compare('material_id',$this->material_id,true);

$criteria->compare('qty',$this->qty);

$criteria->compare('name',$this->name,true);

$criteria->compare('weight',$this->weight,true);

$criteria->compare('square_footage',$this->square_footage,true);

$criteria->compare('cubic_meters',$this->cubic_meters,true);

$criteria->compare('cost',$this->cost,true);


return new CActiveDataProvider(get_class($this), array(

	'criteria'=>$criteria,

));



And in the view:




<?php $this->widget('zii.widgets.grid.CGridView', array(

	'id'=>'bid-detail-grid',

	'dataProvider'=>$model->search($bidId),

	'filter'=>$model,

	'columns'=>array(

		'material_id',

		'qty',

		'name',

		'weight',

		'square_footage',

		'cubic_meters',

		'cost',

		'amount',

	),

)); ?>




Did I do something wrong?

I have the same problem as rei.

I’ve added condition to criteria in model as zaccaria posted, but it still doesn’t allow me to sort by that column.

Has anyone any solution that works and was tested? Or maybe there is something more to do to make it work but it wasn’t mentioned?

I’ve managed to make it work. After adding sort parameter to my dataprovider in model, sorting started throwing errors but it made links for my added columns. When I inspected errors - it told me whats wrong - sorting tried to add table alias to my added columns, which is bad, because they are not part of the table. To solve it I got to comment a bit of code which pastes table alias into order in class CSort:




public function getOrderBy()

	{

		$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=/*CActiveRecord::model($this->modelClass)->getTableAlias(true).'.'.*/$schema->quoteColumnName($attribute);

					}

					$orders[]=$descending?$attribute.' DESC':$attribute;

				}

			}

			return implode(', ',$orders);

		}

	}



so the problem was in line:




$attribute=CActiveRecord::model($this->modelClass)->getTableAlias(true).'.'.$schema->quoteColumnName($attribute);



And here is what needs to be added in Search() function in model class:




$sort = new CSort;

$sort->attributes = array('col1', 'col2', 'col3', /* ect... */ 'added_col1', 'added_col2');


return new CActiveDataProvider(get_class($this), array(

			'criteria'=>$criteria, 'pagination' => $pagination, 'sort'=>$sort,

        ));



As far as I’ve done few tests it seems to work while operating on single table where table alias is not required, but this is just temporary solution because there are most likely cases when it won’t work as it should be.

[EDIT:]

After more investigation I’ve come to better solution, which is checking if attribute is actually in table columns and if it is then we can add table alias, in other case it goes w/o table alias. To do it I needed new field in CSort called $tableName, which is $model->tableName(), and then I can build query to get table columns like in CMysqlSchema->findColumns():




private function isColumnPartOfTable($schema, $columnName)

    {

        if ($this->tableName !== null)

        {

            $sql='SHOW COLUMNS FROM '.$schema->quoteTableName($this->tableName);

            try

            {

                $tableColumns=Yii::app()->db->createCommand($sql)->queryAll();

            }

            catch(Exception $e)

            {

                return false;

            }


            foreach ($tableColumns as $column)

            {

                if ($column['Field'] == $columnName)

                {

                    return true;

                }

            }

        }

        return false;

    }



and then changed:




$attribute=CActiveRecord::model($this->modelClass)->getTableAlias(true).'.'.$schema->quoteColumnName($attribute);



to:




if ($this->isColumnPartOfTable($schema, $attribute))

{

    $attribute=CActiveRecord::model($this->modelClass)->getTableAlias(true).'.'.$schema->quoteColumnName($attribute);

}

else

{

    $attribute=$schema->quoteColumnName($attribute);

}



and another change in CSort is:




    public function __construct($modelClass=null, $tableName=null)

    {

        $this->modelClass=$modelClass;

        $this->tableName= $tableName;

    }



This solution seems to work for me on single table and with joined tables.

I solved this by adding sort option in data provider:




$sort=new CSort;

$sort->attributes=array(

	'qty',

	'cost',

	'markup',

	'amount'=>array(

		'asc'=>'cost*qty',

		'desc'=>'cost*qty desc',

	)

);		

		

return new CActiveDataProvider(get_class($this), array(

	'criteria'=>$criteria,

	'sort'=>$sort

));



And it works fine so far … :rolleyes:

[font="Trebuchet MS"]i have a same problem and with this way… it works…

this has helping me.

there is any other way to combine or merge the existing sort to the new column that assign as a massive way?[/font]

Sorry to bring back this very old thread, but apparently this never got fixed in Yii1.1.

I just wanted to add that for an extra column in the select called ‘amount’ this works too:


$sort->attributes=array(

        'qty',

        'cost',

        'markup',

        'amount'=>array(

                'asc'=>'amount asc',

                'desc'=>'amount desc',

        )

);

No need to put the whole calculation or whatever you have in the select again in the asc/desc array.

Do not forget you also need a ‘public $amount’ in your model.