We probably have all used a virtual attribute (model function) to retrieve complex or related data for a CGridView column:
array(
'name' => ... ,
'value' => '$data->getModelFunction()',
'header' => ... ,
'filter' => ... ,
),
But usually such model function only returns a single value for a single CGridView column. This means that we need to use separate model functions if we need virtual attribute data for multiple CGridView columns.
The problem is that these model functions often have to perform lengthy tasks - such as reading through all related child records. Having multiple virtual attributes of this kind will thus mean a lot of db access for each CGridView column.
The method explained here, boils down to using just one model function, which reads through the related records just once. It then stores all required virtual attribute data in an array, which is stored in the same model that was used to call the search() function, from where it can be read/updated by all the CGridView columns (requires php 5.3 for the "use" keyword).
Suppose you have invoice_tbl and invoice_lines_tbl (one-many). In the gridview you want the following in each row:
column-1: invoice numbers from invoice_tbl (included in dataprovider as usual)
column-2: total amount of each invoice's lines from invoice_lines_tbl (virtual attribute)
column-3: total tax of each invoice's lines from invoice_lines_tbl (virtual attribute)
The function needs to be called by the first column that requires virtual attribute data (column-2). The function reads through the related invoice_lines_tbl only once and performs the necessary calculations for both $amount and $tax, before storing both values in the array.
Column-2 then extracts 'amount' from the array. Column-3 can retrieve the same array and extract 'tax', without needing to call another model function again.
The model function:
public $valuesArray = array();
public function getAmountTax($invoice_id)
{
// Read related records and calculate $amount and $tax
...
$this->valuesArray = array(
'amount'=> $amount,
'tax' => $tax
);
return 'Everything OK';
}
The view:
// The CGridView
$this->widget('zii.widgets.grid.CGridView', array(
...
'columns'=>array(
...
array(
'name' => 'column_2',
'value'=>function($data,$row) use (&$model){
// Populate array
$result = $model->getAmountTax($data->invoice_id);
// Extract value from array
return $model->valuesArray['amount'];
},
...
),
array(
'name' => 'column_3',
'value'=>function($data,$row) use (&$model){
// Extract value from array
return $model->valuesArray['tax'];
},
...
),
IMPORTANT ¶
Just like normal function parameters, the parameter provided to the column's function via the "use" keyword is passed "by value". To be able to update such parameter ($model) in the parent scope (view), it must be passed "by reference". To pass the parameter "by reference", simply add an ampersand (&) in front of the parameter when passed to the "use" language construct (do not use the ampersand inside the function as well).
Obviously, since $model can be updated by each column, this feature allows for more possibilities pertaining to communication between gridview columns, the gridview and its parent view, and maybe also between different widgets in the view.
TIPS ¶
If you want to calculate a virtual attribute only once and use it in all your gridview rows, then store it in $model (the main $model passed to the view which was used to call the search() function to get the dataprovider).
If the virtual attributes need to be calculated for each gridview row, you can store it in $data (the dataprovider contains a model for each gridview row - called $data).
In the gridview column, 'cssClassExpression' is executed before 'value'. So if your headings also need virtual attributes, then call the function in 'cssClassExpression'.
'cssClassExpression' => '$data->getAmountTax($data->invoice_id) ? "" : ""',
It might be handy sometimes to update the models in the dataprovider AFTER it was generated in the search() function.
public $myAttribute;
public function search()
{
...
$dataProvider = new ...
foreach($dataProvider->getData() as $data)
{
$data->myAttribute = 'Updated';
}
return $dataProvider;
}
// In the gridview column you can access it by using $data->myAttribute.
Hope this helps.
Hiding the "cache" in the model
Hi
It a nice reminder of how we can use lambda functions in these cases.
Personnally, I prefer to "hide" this kind of "caching" inside the model.
Yes, in that case you will need to have one getter for each virtual attribute, or you will need to override the 'get' and/or 'call' methods. However, each these methods could internally call 'getAmountTax' which would check if '$virtAttrbArray' inside the model exists and if not, fill it.
Further one can, if appropriate, also store these results in 'Yii::app()->cache'.
Hiding the "cache" in the model
Hi. Thanx for your inputs le_top. It surely is the right thing to do, so I moved the array from the view to the model.
Two related articles and extensions:
In addition to this wiki article im sure this next ones will help you a lot:
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.