You are viewing revision #5 of this wiki article.
This version may not be up to date with the latest version.
You may want to view the differences to the latest version or see the changes made in this revision.
This tip created in conjunction with IRC #yii channel users emilsedgh and tydeas - thanks!
When using [CGridView] to display rows of data, each one is independent of the other - this is normally what we want. But a cases requiring a running total (say, the current balance in a bank account register or a running inventory count) are more tricky because of no shared context.
This How-to introduces the [CGridColumn] component and how to extend it to provide this running total (which may inspire other uses as well).
The sample table ¶
Our example will use a simple inventory table (we'll omit the Model and Controller code, as they are straightforward): ~~~ [sql] CREATE TABLE inventory (
id INTEGER PRIMARY KEY NOT NULL AUTO_INCREMENT,
name VARCHAR(64),
quantity INTEGER
);
insert into inventory values ( 1, 'Beer', 4 ); insert into inventory values ( 2, 'Wine', 2 ); insert into inventory values ( 3, 'Vodka', 7 ); ~~~ Our goal is to show this in a table with all the product detail, plus a running total of the quantities in a fourth column.
Using CGridColumn ¶
To do this, we define that fourth column with our own helper class, TotalColumn
. This extends the grid's column class that does the actual rendering of cell data, and this class has a private variable with that running total.
Our view code is:
<?php
// protected/views/inventory/index.php
Yii::import('zii.widgets.grid.CGridColumn');
class TotalColumn extends CGridColumn {
private $_total = 0;
public function renderDataCellContent($row, $data) { // $row number is ignored
$this->_total += $data->quantity;
echo $this->_total;
}
}
$this->widget('zii.widgets.grid.CGridView', array(
'dataProvider' => $dp, // provided by the controller
'columns' => array(
'id',
'name',
'quantity',
array(
'header' => 'Total',
'class' => 'TotalColumn'
)
)));
This produces a grid with four columns, the last being the running total: just like we wanted.
Some points on this approach:
- The
class
name is a PHP class, not a CSS class, and this creates a new instance ofTotalColumn
, one time perCGridView
, not one time per row. - Because
CGridColumn
is part of the Zii hierarchy and not in the usual import path, we must import the class explicitly so Yii can use it. - It's perfectly acceptable to define this helper class directly in the view. Because the attribute name (
quantity
) is coded directly into the class, this limits its reuse. The next section talks about making this a more general extension. - If the
CGridView
is in some kind of loop within the view, a newTotalColumn
object is created each time around, which resets the running total. No static/global variables to get in the way. - All the other columns defined are of the type
CGridColumn
too, created automatically byCGridView
; they can each be extended in their own way.
Generalizing it ¶
Because the original version hardcodes the attribute name ($data->quantity
), we can't reuse this generally, but we can expand the class a bit to allow assignment of the attribute name as part of the object's creation.
In the same array('class'=>'TotalColumn')
found in the CGridView
, we can add other attribute assignments, including get/set functions we define in the helper class. By adding setAttribute()
and getAttribute()
methods, we can make our column totalizer more general and move it into the protected/components/
folder (and out of the view code).
<?php
// protected/components/TotalColumn.php
Yii::import('zii.widgets.grid.CGridColumn');
class TotalColumn extends CGridColumn {
private $_total = 0;
private $_attr = null;
public function getAttribute()
{
return $this->_attr;
}
public function setAttribute($value)
{
$this->_attr = $value;
}
public function renderDataCellContent($row, $data) {
$this->_total += $data->{$this->attribute};
echo $this->_total;
}
}
This done, we can remove the helper class from the view code and add the attribute name as one of the initializers:
<?php
$this->widget('zii.widgets.grid.CGridView', array(
'dataProvider' => $dp,
'columns' => array(
'id',
'name',
'quantity',
array(
'header' => 'Total',
'class' => 'TotalColumn',
'attribute' => 'quantity', // THIS IS NEW
)
)));
Running totals in SQL ¶
It is possible to do this in SQL directly, but it's very computationally expensive and doesn't fit in well with the Yii way of doing things. But because it's interesting - and people inevitably ask about it - the same query can be performed this way:
mysql> SELECT a.id,
-> a.name,
-> a.quantity,
-> ( SELECT SUM( quantity )
-> FROM inventory b
-> WHERE b.id <= a.id) AS running_qty
-> FROM inventory a
-> ORDER BY id;
+----+-------+----------+-------------+
| id | name | quantity | running_qty |
+----+-------+----------+-------------+
| 1 | Beer | 4 | 4 |
| 2 | Vodka | 2 | 6 |
| 3 | Milk | 7 | 13 |
+----+-------+----------+-------------+
3 rows in set (0.00 sec)
This method gets exponentially slower as the number of rows increases. This is not recommended.
A little bit more ¶
Transaction example ¶
In the above example refers we had an inventory table, instead of it if we may have a transaction table like:
[mysql]
CREATE TABLE transaction (
id INTEGER PRIMARY KEY NOT NULL AUTO_INCREMENT,
quantity INTEGER,
date TIMESTAMP,
);
That would look like:
+----+----------+------------+
| id | quantity | date |
+----+----------+------------+
| 1 | 10 | 2011-03-01 |
| 2 | 10 | 2011-03-10 |
| 3 | -1 | 2011-03-19 |
| 4 | -2 | 2011-03-25 |
| 5 | -9 | 2011-04-01 |
+----+----------+------------+
The above simple example will work here as well with no problem. But what will happen if the user wants to add a from date to date condition in the criteria that init the dataprovider for the view? Because the TotalColumn will always start with $_total = 0; the result will be false. What we can do is alter the TotalColumn code and add setter and getter for the total value like:
<?php
// protected/components/TotalColumn.php
Yii::import('zii.widgets.grid.CGridColumn');
class TotalColumn extends CGridColumn {
private $_total = 0;
private $_attr = null;
public function getTotal()
{
$this->_total;
}
public function setTotal($value)
{
$this->_total = $value;
}
public function getAttribute()
{
return $this->_attr;
}
public function setAttribute($value)
{
$this->_attr = $value;
}
public function renderDataCellContent($row, $data) {
$this->_total += $data->{$this->attribute};
echo $this->_total;
}
}
And doing the following will result in a proper transaction grid:
$criteria = new CDbCriteria();
$criteria->addCondition('date > 2011-03-20');
$dp = new CActiveDataProvider('Transaction', array('criteria'=>$criteria));
$this->widget('zii.widgets.grid.CGridView', array(
'dataProvider' => $dp,
'columns' => array(
'id',
'name',
'quantity',
array(
'header' => 'Total',
'class' => 'TotalColumn',
'attribute' => 'quantity', // THIS IS NEW
'total' => 19, // The true total for date < 2011-03-20
)
)));
Extra ¶
Its a common case that you will need a numeric format for this "running total" field. Because we extend CGridColumn
we don't have access to the CDataColumn::$type.
So, if you want to be able to use 'type'=>'number'
so the running total has a numeric format you must alter the class TotalColumn
like this
class TotalColumn extends CGridColumn
{
...
public $type = 'number'; //If you want to be able to access it.
...
public function renderDataCellContent($row, $data) {
$this->_total += $data->{$this->attribute};
echo (isset($this->type)) ? $ $this->grid->getFormatter()->format($this->_total, $this->type) : this->_total;
}
}
Awesome!
Firstly, very well written article!
Secondly, you're a great help in Yii chat!
Thirdly, excellent wiki! I'm in the financial world by profession and this is a common functionality. I can't wait to implement it!
Thank you so much for everything!!!
For array or sql data provider
Nice article!
If using CSqlDataProvider or CArrayDataProvider replace:
$this->_total += $data->{$this->attribute};
with
$this->_total += $data[$this->attribute];
For array or sql data provider
i would just use
$this->_total += CHtml::value($data,$this->attribute);
this works with sql or array dataprovider
Perfect, but if you want a footer with the sum.!
You can refer to this post, where we'll added a footer with the sum of a column.
http://www.yiiframework.com/forum/index.php/topic/41383-yii-cgridview-footer-with-sum-of-column-values
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.