Difference between #6 and #5 of Keeping a running total in a CGridView

unchanged
Title
Keeping a running total in a CGridView
unchanged
Category
How-tos
unchanged
Tags
CGridView
changed
Content
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]
<?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 of `TotalColumn`, one time per `CGridView`, 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 new
`TotalColumn` 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 by `CGridView`; 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]
<?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]
<?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:
~~~
[php]
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]

<?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:

~~~
[php]

$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 <code>CGridColumn</code> we
don't have access to the
<code>[CDataColumn::$type](http://www.yiiframework.com/doc/api/1.1/CDataColumn#type-detail).
</code>
So, if you want to be able to use <code>'type'=>'number'</code>
so the running total has a numeric format you must alter the <code>class
TotalColumn</code> like this 
~~~
[php]
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;
    }
}
~~~