Zend Framework: Automatically fetch the grand total from a limit query
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 ?>


August 27th, 2008 at 7:27 am
Thanks for sharing this!
Unfortunately, I haven’t been able to use the code as-is, I was getting “Select query cannot join with another table” PDO exception.
I had to extend Zend_Db_Table_Select and add a method to custom set $_parts[] property:
class MySite_Db_Table_Select extends Zend_Db_Table_Select
{
public function setPart($correlation, $part, $col)
{
array_push($this->_parts[$part], array($correlation, $col, null));
}
}
And:
$select->reset(Zend_Db_Select::LIMIT_COUNT)
->reset(Zend_Db_Select::LIMIT_OFFSET)
->reset(Zend_Db_Select::ORDER)
->reset(Zend_Db_Select::COLUMNS)
->setPart($this->_name, Zend_Db_Select::COLUMNS, new Zend_Db_Expr(‘COUNT(1)’));
Any idea why your code wasn’t working? I am using ZF v1.6RC1.
It looks like the problem lies in null argument of
->from(null, ‘COUNT(1)’);
Setting NULL creates problem in Zend\Db\Table\Select.php -> assemble() method:
foreach ($fields as $columnEntry) {
/**
* $columnEntry is: array(false, Zend_Db_Expr(‘COUNT(1)’), false). First ‘false’ is
* the result of passing NULL to $select->from().
*/
list($table, $column) = $columnEntry;
if ($column) {
if (!isset($from[$table]) || $from[$table]['tableName'] != $primary) {
// We end up here because list() above sets $table to NULL.
throw new Zend_Db_Table_Select_Exception(‘Select query cannot join with another table’);
}
}
}
Now, passing the same table name to $select->from() as:
->from(‘myTable’, ‘COUNT(1)’);
or
->from($this->_name, ‘COUNT(1)’);
creates a second element in $select->_parts['from'] with a new correlation name “myTable_2″. This forces Zend_Db to join the same table again (using “myTable_2″ alias) and that causes PDO exception saying that field name is ambiguous.
I am really curious to know how all that works for you. Maybe we could figure out correct way of doing it.
What’s your ZF version?
Thanks
August 27th, 2008 at 9:04 am
This is for ZF 1.5.3 which doesn’t contain a Zend_Db_Select::assemble() method. Looks like I’ll have to keep an eye out for this, thanks for the heads up.
Passing a null $name to Zend_Db_Select::from() seemed to be the only way to re-specify the column selection for an existing “from” clause.
As soon as 1.6 goes stable, I’ll check it out and update this post with any changes.
September 11th, 2008 at 12:47 pm
Zend have now graciously added the Zend_Db_Select::columns() method. Might also want to check out Zend_Paginator.
September 18th, 2008 at 11:49 pm
Thanks a lot for the update. Will test it asap.
what is the practical importance of Zend_Db_Select::columns()? Does it have a connection to the topic of this article?
September 19th, 2008 at 12:44 am
Disregard my previous post, I didn’t notice:
->columns(‘COUNT(1)’);
Thanks again.
May 27th, 2009 at 1:01 am
Great tutorial. You may want to add ->reset(Zend_Db_Select::GROUP) to get your code working with a grouped query.
Regards Rafael