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](http://www.symfony-project.com/book/trunk/08-Inside-the-Model-Layer) 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:

    [php]
    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:

    [php]
    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]
    <?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]
    <?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]
    <?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:

&nbsp;&nbsp;&nbsp;[&laquo;](#) [&lt;](#) [1](#) - 2 - [3](#) - [4](#) - [5](#) [&gt;](#) [&raquo;](#)

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.

>**Tip**: The above code is automated by the `sfPagerNavigation` plugin. Refer to its [description page](http://www.symfony-project.com/trac/wiki/sfPagerNavigationPlugin) for more information on installation and use.

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]
    <?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:

    [php]
    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:

    [php]
    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:

    [php]
    <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:

    [php]
    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:

    [php]
    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.

    [php]
    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:

    [php]
    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:

    [php]
    $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:

    [php]
    $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:

    [php]
    $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](http://www.symfony-project.com/book/trunk/02-Exploring-Symfony-s-Code#Parameter%20Holders).