September 11, 2008 – Updated for Zend Framework 1.6

So you’ve got a nice limited result set for use in a paginated display. The thing with most pagination widgets is they need to know the grand total for determining the total number of pages.

What you would normally have to do is manually issue a second query to fetch the total based on the same parameters as the original query, minus the limit and offset (and order but that’s beside the point). Wouldn’t it be nice if this could all be done behind the scenes?

Enter the More Cowbell solution!

For the example, I’ll be assuming you’re using an implementation of the Zend_Db_Table_Abstract class and associated Zend_Db_Table_Rowset_Abstract however this solution is reasonably generic and can be easily applied to other situations.

First things first, lets extend the rowset class.

<?php
class MoreCowbell_Db_Table_Rowset extends Zend_Db_Table_Rowset_Abstract
{
    /**
     * Grande total for limited result sets
     * 
     * @var int
     */
    protected $_total;
 
    /**
     * Constructor
     * 
     * @param array $config
     */
    public function __construct(array $config)
    {
        parent::__construct($config);
 
        if(isset($config['total']) {
            $this->_total = (int) $config['total'];
        } else {
            $this->_total = $this->_count;
        }
    }
 
    /**
     * Returns the grande total
     *
     * @return int
     */
    public function total()
    {
        return $this->_total;
    }
}
?>

This simple extension allows us to store an arbitrary integer representing the grande total inside the resultset object. Now, on to the meat and potatoes.

Your usual Zend_Db_Table_Abstract implementation is a fairly simple affair; define a table name, primary key(s), etc. We’re going to add just a little more abstraction for your tables to inherit.

<?php
abstract class MoreCowbell_Db_Table_Abstract extends Zend_Db_Table_Abstract
{
    /**
     * Classname for rowset
     *
     * @var string
     */
    protected $_rowsetClass = 'MoreCowbell_Db_Table_Rowset';
 
    /**
     * Fetches all rows.
     *
     * Honors the Zend_Db_Adapter fetch mode.
     *
     * @param string|array|Zend_Db_Table_Select $where  OPTIONAL An SQL WHERE clause or Zend_Db_Table_Select object.
     * @param string|array                      $order  OPTIONAL An SQL ORDER clause.
     * @param int                               $count  OPTIONAL An SQL LIMIT count.
     * @param int                               $offset OPTIONAL An SQL LIMIT offset.
     * @return Zend_Db_Table_Rowset_Abstract The row results per the Zend_Db_Adapter fetch mode.
     */
    public function fetchAll($where = null, $order = null, $count = null, $offset = null)
    {
        // This is all straight out of Zend_Db_Table_Abstract::fetchAll()
 
        if (!($where instanceof Zend_Db_Table_Select)) {
            $select = $this->select();
 
            if ($where !== null) {
                $this->_where($select, $where);
            }
 
            if ($order !== null) {
                $this->_order($select, $order);
            }
 
            if ($count !== null || $offset !== null) {
                $select->limit($count, $offset);
            }
 
        } else {
            $select = $where;
        }
 
        $rows = $this->_fetch($select);
 
        $data  = array(
            'table'    => $this,
            'data'     => $rows,
            'readOnly' => $select->isReadOnly(),
            'rowClass' => $this->_rowClass,
            'stored'   => true
        );
 
        // Here's where it gets interesting
        if ($select->getPart(Zend_Db_Select::LIMIT_COUNT) || $select->getPart(Zend_Db_Select::LIMIT_OFFSET)) {
            // Limit exists, reset unnecessary parameters and columns and re-issue query returning the count
            $select->reset(Zend_Db_Select::LIMIT_COUNT)
                   ->reset(Zend_Db_Select::LIMIT_OFFSET)
                   ->reset(Zend_Db_Select::ORDER)
                   ->reset(Zend_Db_Select::COLUMNS)
                   ->columns('COUNT(1)');
 
            $data['total'] = (int) $this->_db->fetchOne($select);
        }
 
        @Zend_Loader::loadClass($this->_rowsetClass);
        return new $this->_rowsetClass($data);
    }
}
?>

That’s it! I opted to reiterate a large chunk of Zend_Db_Table_Abstract::fetchAll() simply because we’d have to build the Zend_Db_Table_Select object anyway so why do it twice.

When you create your table objects, extend MoreCowbell_Db_Table_Abstract instead of Zend_Db_Table_Abstract. You can then use your table object as usual with the added bonus of if you issue a limit query that returns a resultset, you now have access to the total number of matches. Here’s a quick example.

<?php
class My_Table extends MoreCowbell_Db_Table_Abstract
{
    protected $_name = 'my_table';
 
    protected $_primary = 'id';
}
?>
<?php
 
$table = new My_Table;
 
$select = $table->select()->where('status = ?', 'NEW')
                          ->limit(10, 20);
 
// Returns rows 21 through 30
// In this example we'll assume there are 50 'NEW' rows present
 
$rows = $table->fetchAll($select);
 
$count = $rows->count(); // 10
$total = $rows->total(); // 50
 
?>