How to paginate a list
Overview
Symfony provides a pager component: the sfPropelPager
object. It can separate a list of results from a criteria object (of Criteria
class) into a set of pages for display, and offers access methods to pages and result objects.
The sfPropelPager
object
The sfPropelPager
class uses the Propel abstraction layer, as described in the Model chapter.
This chapter will illustrate the way to use the sfPropelPager
methods with a simple example : displaying a list of articles ten by ten. Assume that the Article
object has getPublished()
, getTitle()
, getOverview()
and getContent()
accessor methods.
If you wanted to have the non-paginated result of a criteria request showing only published articles, you would need:
class articleActions extends sfActions { public function executeList() { ... $c = new Criteria(); $c->add(ArticlePeer::PUBLISHED, true); $articles = ArticlePeer::doSelect($c); $this->articles = $articles; ... } }
The $articles
variable, accessible from the template, would contain an array of all the Article
objects matching the request.
To get a paginated list, you need a slightly different approach; the results have to be put in a sfPropelPager
object instead of an array:
class articleActions extends sfActions { public function executeList() { ... $c = new Criteria(); $c->add(ArticlePeer::PUBLISHED, true); $pager = new sfPropelPager('Article', 10); $pager->setCriteria($c); $pager->setPage($this->getRequestParameter('page', 1)); $pager->init(); $this->pager = $pager; ... } }
The differences start after the criteria definition, since this action:
- creates a new pager to paginate
Article
objects ten by ten - affects the criteria to the pager
- sets the current page to the requested page or to the first one
- initializes the pager (i.e. execute the request related to the criteria)
- passes the pager to the template via the
$pager
variable
The template listSuccess.php
can now access the sfPropelPager
object. This object knows the current page and the list of all the pages. It has methods to access pages and objects in pages. Let's see how to manipulate it.
To display the total number of results, use the getNbResults()
method:
<?php echo $pager->getNbResults() ?> results found.<br /> Displaying results <?php echo $pager->getFirstIndice() ?> to <?php echo $pager->getLastIndice() ?>.
To display the articles of the requested page, use the getResults()
method of the pager
object to retrieve the objects in the page:
<?php foreach ($pager->getResults() as $article): ?> <?php echo link_to($article->getTitle(), 'article/read?id='.$article->getId()) ?> <?php echo $article->getOverview() ?> <?php endforeach ?>
Navigating across pages
The pager object knows if the number of results exceeds the maximum number that can be displayed in one page (10 in this example) thanks to the haveToPaginate()
method.
To add the page navigation links at the bottom of the list (« < > »), use the navigation methods getFirstPage()
, getPreviousPage()
, getNextPage()
and getLastPage()
. The current page is given by getPage()
. All these methods return an integer: the rank of the requested page.
To point to a specific page, loop through the collection of links obtained by a call to the getLinks()
method:
<?php if ($pager->haveToPaginate()): ?> <?php echo link_to('«', 'article/list?page='.$pager->getFirstPage()) ?> <?php echo link_to('<', 'article/list?page='.$pager->getPreviousPage()) ?> <?php $links = $pager->getLinks(); foreach ($links as $page): ?> <?php echo ($page == $pager->getPage()) ? $page : link_to($page, 'article/list?page='.$page) ?> <?php if ($page != $pager->getCurrentMaxLink()): ?> - <?php endif ?> <?php endforeach ?> <?php echo link_to('>', 'article/list?page='.$pager->getNextPage()) ?> <?php echo link_to('»', 'article/list?page='.$pager->getLastPage()) ?> <?php endif ?>
This should render something like:
Once the article displayed, to allow a direct navigation to the previous or the next article without going back to the paginated list, you will need a cursor.
Navigating across objects
Navigating page by page within the list is easy, but the users might not want to go back to the list to navigate object by object. The cursor
attribute of the sfPropelPager
object can hold the offset of the current object.
This will allow an article by article navigation in the readSuccess.php
template. First, let's modify a bit of the code of the listSuccess.php
template:
<?php $cursor = $pager->getFirstIndice(); foreach ($pager->getResults() as $article): ?> <?php echo link_to($article->getTitle(), 'article/read?cursor='.$cursor) ?> <?php echo $article->getOverview() ?> <?php ++$cursor; endforeach ?>
The read
action will need to know how to handle a cursor
parameter:
class articleActions extends sfActions { public function executeRead() { ... if ($this->getRequestParameter('cursor')) { $article = $pager->getObjectByCursor($this->getRequestParameter('cursor')); } else if ($this->getRequestParameter('id')) { $article = ArticlePeer::retrieveByPK($this->getRequestParameter('id')); } // Error $this->forward404Unless($article); } }
The getObjectByCursor($cursor)
method sets the cursor at a specified position and returns the object at that very position.
You can set the cursor without getting the resulting object with the setCursor($cursor)
method. And once the cursor is set, you can grab the current object at this position (getCurrent()
) but also the previous one (getPrevious()
) and the next one (getNext()
).
This means that the read
action can pass to the template the necessary information for an article-by-article navigation with a few modifications:
class articleActions extends sfActions { public function executeRead() { ... if ($this->getRequestParameter('cursor')) { $pager->setCursor($this->getRequestParameter('cursor')); $previous_article = $pager->getPrevious(); $article = $pager->getCurrent(); $next_article = $pager->getNext(); } else if ($this->getRequestParameter('id')) { $article = ArticlePeer::retrieveByPK($this->getRequestParameter('id')); } // Error $this->forward404Unless($article); } }
note
The getPrevious()
and getNext()
methods return null
if there is no previous or next object.
The readSuccess.php
template could look like:
<h1><?php echo $article->getTitle() ?></h1> <p class="overview"><?php echo $article->getOverview() ?></p> <div class="content"> <?php echo $article->getContent() ?> </div> < <?php echo link_to_if($previous_article, $previous_article->getTitle(), 'article/read?id='.$previous_article->getId()) ?> - > <?php echo link_to_if($next_article, $next_article->getTitle(), 'article/read?id='.$next_article->getId()) ?>
Changing the sort order
As the sfPropelPager
object relies on a Criteria
object, changing the order of the pager is simply done by adding a sort to the criteria, before assigning it to the pager object.
For instance, you can add the choice of the sort column to the list navigation interface:
class articleActions extends sfActions { public function executeList() { ... $c = new Criteria(); $c->add(ArticlePeer::PUBLISHED, true); if ($this->getRequestParameter('sort')) { $c->addAscendingOrderByColumn(ArticlePeer::translateFieldName($this->getRequestParameter('sort'), BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_COLNAME)); } else { // sorted by date by default $c->addAscendingOrderByColumn(ArticlePeer::UPDATED_AT); } $pager = new sfPropelPager('Article', 10); $pager->setCriteria($c); $pager->init(); $this->pager = $pager; ... } }
Add the following to the listSuccess.php
template:
Sort by : <?php echo link_to('Title', 'article/list?sort=title') ?> - <?php echo link_to('Id', 'article/list?sort=Id') ?>
Changing the number of results per page
The setMaxPerPage($max)
method changes the number of results displayed per page, without the need to reprocess the pager (no need to call init()
again). If you pass the value 0
for parameter, the pager will display all the results in one single page.
class articleActions extends sfActions { public function executeList() { ... $c = new Criteria(); $c->add(ArticlePeer::PUBLISHED, true); $pager = new sfPropelPager('Article', 10); $pager->setCriteria($c); if ($this->getRequestParameter('maxperpage')) { $pager->setMaxPerPage($this->getRequestParameter('maxperpage')); } $pager->init(); $this->pager = $pager; ... } }
So you can add the following to the listSuccess.php
template:
Display : <?php echo link_to('10', 'article/list?maxperpage=10') ?> - <?php echo link_to('20', 'article/list?maxperpage=20') ?> results per page
Changing the select method
If you need to optimize the performance of an action relying on a sfPropelPager
, you might want to force the pager to use a doSelectJoinXXX()
instead of a simple doSelect()
. This is easily achieved by the setPeerMethod()
method of the sfPropelPager
object:
$pager->setPeerMethod('doSelectJoinUser');
Note that the pager actually processes the doSelect()
query when displaying a page. The first query (triggered by $pager->init()
) does only a doCount
, and you can also customize this method by calling:
$pager->setPeerCountMethod('doCountJoinUser');
Storing additional information in the pager
You may need sometimes to keep a certain context in a pager object. That's why the sfPropelPager
class can handle parameters in the usual way:
$pager->setParameter('foo', 'bar'); if ($pager->hasParameter('foo')) { $pager->getParameter('foo'); $pager->getParameterHolder()->removeParameter('foo'); } $pager->getParameterHolder()->clearParameters();
These parameters are never used directly by the pager.
To learn more about custom parameters, refer to the Chapter 2.
This work is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.