Exploring Zend_Paginator
One area of displaying lists on web pages that I’ve generally disliked doing is pagination as it’s a bit of a faff. Recently, I needed to do just this though as I couldn’t delegate it as my colleague was too busy on other work. As a result, I thought that I should look into Zend_Paginator this time. Turns out that it’s really easy to use and the documentation is great too.
The really useful thing about Zend_Paginator is that it uses adapters to collect its data. There are a variety of adapters, including array, dbSelect, dbTableSelect and iterator. The interesting ones for me being dbSelect and dbTableSelect as I use Zend_Db based data access layers.
This is how I used it with a Zend_Db based data mapper within TodoIt.
Setting up the paginator
My current method looks like this:
class Application_Model_TaskMapper
{
public function fetchOutstanding()
{
$db = $this->getDbAdapter();
$select = $db->select();
$select->from($this->_tableName);
$select->where('date_completed IS NULL');
$select->order(array('due_date ASC', 'id DESC'));
$rows = $db->fetchAll($select);
foreach ($rows as $row) {
$task = new Application_Model_Task($row);
$tasks[] = $task;
}
return $tasks;
}
// etc
This is pretty standard code for a data mapper. We select the data from the database and convert it to an array of entities. For the paginator to do its stuff though, we have to pass it the select object so that it can set the limit() on the select object.
The code therefore becomes:
public function fetchOutstanding()
{
$db = $this->getDbAdapter();
$select = $db->select();
$select->from($this->_tableName);
$select->where('date_completed IS NULL');
$select->order(array('date_completed DESC', 'id DESC'));
$adapter = new Zend_Paginator_Adapter_DbSelect($select);
$paginator = new Zend_Paginator($adapter);
return $paginator;
}
As you can see, we create an instance of Zend_Paginator_Adapter_DbSelect which takes the $select object and the instantiate a Zend_Paginator and return it. The Zend_Paginator object implements Interator, so you can use it exactly like an array in a foreach loop and hence, in theory, your view script doesn’t need to change.
However, the code that consumes TaskMapper expects an array of Task objects, not an array of arrays. To tell the paginator to create our objects, we extend Zend_Paginator_Adapter_DbSelect and override getItems() like this:
class Application_Model_Paginator_TaskAdapter extends Zend_Paginator_Adapter_DbSelect
{
/**
* Returns an array of items for a page.
*
* @param integer $offset Page offset
* @param integer $itemCountPerPage Number of items per page
* @return array
*/
public function getItems($offset, $itemCountPerPage)
{
$rows = parent::getItems($offset, $itemCountPerPage);
$tasks = array();
foreach ($rows as $row) {
$task = new Application_Model_Task($row);
$tasks[] = $task;
}
return $tasks;
}
}
Here, we’ve used the entity-creation code that was in our original implementation of fetchOutstanding() and placed it in getItems().
Obviously we have to update fetchOutstanding() to use our new adapter, so we replace
$adapter = new Zend_Paginator_Adapter_DbSelect($select);
with $adapter = new Application_Model_Paginator_TaskAdapter($select);
Now, when we iterate over the pagination object, we get instances of Task and all is well with the world.
Using the paginator
Now that we have a paginator in place, we need to use it. Specifically we need to tell the paginator which page number we want to view and how many items are on a page. Within TodoIt, this is done in the ServiceLayer object and looks something like this:
class Application_Service_TaskService
{
// ...
public function fetchOutstanding($page, $numberPerPage = 25)
{
$mapper = new Application_Model_TaskMapper();
$tasks = $mapper->fetchOutstanding();
$tasks->setCurrentPageNumber($page);
$tasks->setItemCountPerPage($numberPerPage);
return $tasks;
}
// ...
Clearly the $page parameter comes via the URL at some point, so the controller looks something like this:
class IndexController extends Zend_Controller_Action
{
public function indexAction()
{
$page = $this->_getParam('page', 1);
$taskService = new Application_Service_TaskService();
$this->view->outstandingTasks = $taskService->fetchOutstanding($page);
$messenger = $this->_helper->flashMessenger;
$this->view->messages = $messenger->getMessages();
}
//...
and then the view uses a foreach as you’d expect.
Adding the paging controls
Finally, to complete a paged list, we have to provide the user a mechanism to select the next and previous pages along with maybe jumping to a specific page. This is done using a separate view script that you pass to the paginator. In your view script, you put something like:
<?php echo $this->paginationControl($this-> outstandingTasks,
'Sliding',
'pagination_control.phtml'); ?>
The first parameter is your paginator object. The second is the ‘scrolling style’ to use. There are four choices documented in the manual: All, Elastic, Jumping and Sliding. Personally, I have chosen to not display the page numbers themselves, so it doesn’t matter which one I pick. The last parameter is the partial view script that you want to be rendered. This allows you to have complete customisation of the HTML.
Here’s what I’m using which is based heavily on and example in the documentation:
<?php if ($this->pageCount): ?>
<div class="pagination-control">
<!-- Previous page link -->
<?php if (isset($this->previous)): ?>
<a href="<?php echo $this->url(array('page' => $this->previous)); ?>">
Previous
</a> |
<?php else: ?>
<span class="disabled">< Previous</span> |
<?php endif; ?>
<!-- Next page link -->
<?php if (isset($this->next)): ?>
<a href="<?php echo $this->url(array('page' => $this->next)); ?>">
Next >
</a>
<?php else: ?>
<span class="disabled">Next ></span>
<?php endif; ?>
<span class="pagecount">
Page <?php echo $this->current; ?> of <?php echo $this->pageCount; ?>
</span>
</div>
<?php endif; ?>
And that’s it; I now have paginated tasks in TodoIt and as you can see, Zend_Paginator is very easy to use and, more importantly, simple to customise to your own needs.
That is great but you will need to write new pagination adapter for every mapper. I have found another solution.
I have declared collection class for a set of models and filter to convert array to the given collection type. Then I've just extend paginator with addFilter method to add ability to paginator to have more then one filter.
Paginator: https://github.com/tankist/zend-tool-model/blob/master/Skaya/Paginator.php
Example:
https://gist.github.com/954971
Hi Tankist,
That's a nice solution. For my 'big' project I added a method to my adapter to allow me to set the class to use when hydrating the objects which also works well and isn't a big change from the code presented here. I didn't discuss it within the article as it's a fairly simple extension. Maybe I'll write a a followup :)
Regards,
Rob…
Thank you for this excellent post. It is just what we needed and it comes on the day we need it. You couldn't have had better timing.
Rob,
Glad to see someone talking about pagination. Seems to be one of the last things added to result pages yet it is one of the most important from a usability standpoint.
Also, interesting conversation. It's nice to see a few different approaches. My approach is generally to have a service layer super-class which handles providing all hydrated result-sets wrapped in a paginator. This works well because all I have to do is pass the service my criteria object, and it returns paginated results. In places where I don't need paginated results (this isn't often), I just tell the service not to limit the result-set. I still get a paginator, but it then serves as a non-paginated iterator.
Great post. Just what I needed!
thanks for your how to article
i have two questions: 1) i am assuming you are using these ZF components as part of the ZF framework, is that correct? i know people sometimes just use pieces of the ZF and not the entirety … and 2) is your Service class placed in your Model?
many thanks
b
Fatal error: Class 'Application_Service_CoursesService' not found in /var/www/html/zend_dev/application/controllers/CoursesController.php on line 15
any ideas how i might correct this? zf error messages seem cryptic and full of danger
also … what is '$acl'? is that variable but don't see where it is declared
b_dubb,
Yes, I'm using this in the context of a ZF MVC website.
Application_Service_CoursesService needs to live in application/services/CoursesService.php.
Ignore $acl – it's not needed and so I've removed it from the post.
Regards,
Rob…
Good article, thank you. I have a wish ;) if you could write an article similar to this one, about exploring zend_navigation this would be great, i don't know how to use if all of it's magic especially combined with routes xml files and acl rules.
Hi,
I have not tried this solution (but I think it should work) : what about adding a callback using the function setFilter in your mapper. Something like (in Application_Model_TaskMapper) :
public function fetchOutstanding()
{
// …
$paginator->setFilter(new Zend_Filter_Callback, array('Application_Model_TaskMapper', 'map'));
Then you write the function 'map' in your mapper, that receives a Rowset, and create the tasks :
public function map(Zend_Db_Table_Rowset rowset)
{
foreach($rowset as $row)
$task = new Application_Model_Task($row);
$tasks[] = $task;
return $tasks;
};
hi
what about Zend_Paginator_Adapter_DbTableSelect . look like it do the same stuff:
$tasks = array();
foreach ($rows as $row) {
$task = new Application_Model_Task($row);
$tasks[] = $task;
}
return $tasks
also, you may use rowset (not raw array) with it.
Hi,
I read many tutorials on your blog and all of these are really helpfull,i appriciate your struggle.
I have one question regarding Data mapper,Can you please explain it?What are the pros/crons of this technique?
I studied many tutorials on the net but couldnt understand.
hi
Nice post, you can also get the code from here too.
http://www.scriptvenue.com/2011/11/zend-pagination/
thanks