Caution: You are browsing the legacy 1.x part of this website.
This version of symfony is not maintained anymore. If some of your projects still use this version, consider upgrading.
This work is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.

Master Symfony2 fundamentals

Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).
trainings.sensiolabs.com

Discover the SensioLabs Support

Access to the SensioLabs Competency Center for an exclusive and tailor-made support on Symfony
sensiolabs.com

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('&laquo;', 'article/list?page='.$pager->getFirstPage()) ?>
  <?php echo link_to('&lt;', '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('&gt;', 'article/list?page='.$pager->getNextPage()) ?>
  <?php echo link_to('&raquo;', 'article/list?page='.$pager->getLastPage()) ?>
<?php endif ?>

This should render something like:

   « < 1 - 2 - 3 - 4 - 5 > »

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>
&lt; <?php echo link_to_if($previous_article, $previous_article->getTitle(), 'article/read?id='.$previous_article->getId()) ?>
-
&gt; <?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.