La pagination avec Symfony et Doctrine

Créer une liste de résultats avec une pagination est très simple avec Symfony. Pour suivre ce petit tuto, je pars du principe que vous avez déjà créé votre modèle et le module qui lui est associé, tout ce qu’il reste à faire est d’afficher les résultats sur la page d’index du module en intégrant un système de pagination.

Création d’une méthode dans le modèle

Bon plutôt que d’utiliser des noms factices, j’ai pris un exemple banal: on veux afficher une liste de news avec les informations sur la news ainsi que le nom de la catégorie et de l’auteur qui sont stockés dans des tables liées. La première chose à faire est donc de créer la fonction chargée de récupérer les données dans la base.

// class NewsTable extends Doctrine_Table
public function getActiveNews()
{
    $q = $this->createQuery('n')
      ->select('n.*, c.*, a.id, a.username')
      ->from('News n')
      ->innerJoin('n.Category c')
      ->innerJoin('n.Author a')
      ->where('n.is_activated = ?', true)
      ->orderBy('n.created_at DESC');
     
    return $q;
}

Que fait cette requête ? Elle sélectionne toutes les news actives (on considère qu’une news est active si le champ is_activated est égal à 1), les trie en ordre décroissant par date de création et récupère les données liées dans d’autres tables (la catégorie et le membre qui a posté la news) avec les jointures.

J’utilise innerJoin car dans mon modèle une news a forcément une catégorie et un auteur, si ce n’est pas le cas utilisez leftJoin. En faisant les jointures dans cette fonction on diminue de manière drastique le nombre de requêtes pour l’affichage de la liste.

Notez aussi qu’avec le Doctrine pager, la fonction doit renvoyer $q et pas $q->execute()

Création de l’action dans le contrôleur

Lorsque l’on créer un module en utilisant les lignes de commandes, Symfony génère déjà une requête par défaut pour l’action « index », mais on ne va pas l’utiliser puisque l’on vient de créer une fonction exprès pour ça ! De toute façon en principe il faut écrire les requêtes dans le modèle et pas dans le contrôleur. En plus, cela permet de réutiliser la requête dans un autre module sans avoir à la réécrire.

Avant on va juste faire un petit ajout dans le fichier app.yml de votre application :

all:
  news_per_page
: 10

Bien on peut maintenant créer notre action dans le contrôleur du module (actions.php)

  public function executeIndex(sfWebRequest $request)
  {      
    $this->pager = new sfDoctrinePager('News', sfConfig::get('app_news_per_page'));
    $this->pager->setQuery(Doctrine_Core::getTable('News')->getActiveNews());
    $this->pager->setPage($request->getParameter('page', 1));
    $this->pager->init();
  }

Simple non? Il ne reste plus qu’à gérer l’affichage dans la vue indexSuccess.php. On va d’abord afficher les résultats (via un pauvre tableau mais bon c’est pour l’exemple)

<table>
  <thead>
    <tr>
      <th>Title</th>
      <th>Content</th>
      <th>Category</th>
      <th>Author</th>
      <th>Created at</th>
    </tr>
  </thead>
  <tbody>
    <?php foreach ($pager->getResults() as $news): ?>
    <tr>
      <td><a href="<?php echo url_for('news/show?id='.$news->getId()) ?>"><?php echo $news->getTitle() ?></a></td>
      <td><?php echo $news->getContent() ?></td>
      <td><?php echo $news->getCategory()->getName() ?></td>
      <td><?php echo $news->getAuthor()->getUsername() ?></td>
      <td><?php echo $news->getCreatedAt() ?></td>
    </tr>
    <?php endforeach; ?>
  </tbody>
</table>

Et maintenant, toujours dans le même fichier, la pagination en bas du tableau :

<?php if ($pager->haveToPaginate()): ?>
  <div class="pagination">
    <a href="<?php echo url_for('news', $news) ?>?page=<?php echo $pager->getFirstPage() ?>">Premier</a>
    <a href="<?php echo url_for('news', $news) ?>?page=<?php echo $pager->getPreviousPage() ?>">Précédent</a>
 
    <?php foreach ($pager->getLinks() as $page): ?>
      <?php if ($page == $pager->getPage()): ?>
        <?php echo $page ?>
      <?php else: ?>
        <a href="<?php echo url_for('news', $news) ?>?page=<?php echo $page ?>"><?php echo $page ?></a>
      <?php endif; ?>
    <?php endforeach; ?>
 
    <a href="<?php echo url_for('news', $news) ?>?page=<?php echo $pager->getNextPage() ?>">Suivant</a>
    <a href="<?php echo url_for('news', $news) ?>?page=<?php echo $pager->getLastPage() ?>">Dernier</a>
  </div>
<?php endif; ?>

Vous êtes libres de styliser comme bon vous semble les liens en mettant des images, des effets…

Optimisations

On a un système qui fonctionne mais on peut encore un peu l’améliorer. Si on a plusieurs modules avec des listes paginées il peut être intéressant de créer un template unique pour l’affichage des liens vers les autres pages.

Pour ce faire, vous pouvez créer un fichier _paginate.php dans le dossier /templates de votre application. Par exemple, si votre application s’appelle « frontend » cela donne /frontend/templates, il doit déjà y avoir un fichier layout.php à cet endroit normalement. Une fois le fichier crée, copiez-y le code suivant :

<?php if ($pager->haveToPaginate()): ?>
  <div class="pagination">
    <?php echo link_to('Premier', $route.'?page=' . $pager->getFirstPage()) ?>
    <?php echo link_to('Précédent', $route.'?page=' . $pager->getPreviousPage()) ?>
 
    <?php foreach ($pager->getLinks() as $page): ?>
      <?php if ($page == $pager->getPage()): ?>
        <?php echo $page ?>
      <?php else: ?>
        <?php echo link_to($page, $route.'?page=' . $page) ?>
      <?php endif; ?>
    <?php endforeach; ?>
 
    <?php echo link_to('Suivant', $route.'?page=' . $pager->getNextPage()) ?>
    <?php echo link_to('Dernier', $route.'?page=' . $pager->getLastPage()) ?>
  </div>
<?php endif; ?>

Vous pouvez désormais appeler le partial _paginate.php depuis n’importe quelle vue en ajoutant le code suivant dans votre vue :

<?php include_partial('global/paginate', array('pager' => $pager, 'route' => '@news_pagination')) ?>

Seule la route est variable dans le code ci-dessus. Pour que cela marche, il faut éditer le fichier routing.yml de votre application et rajouter autant de routes que nécessaires pour l’affichage des listes. Dans cet exemple cela donne :

news_pagination:
  url
:    /news/page/:page
  class
:  sfDoctrineRoute
  options
: { model: News, type: list }
  param
:  { module: news, action: index }
  requirements
:
    page
: \d+
    sf_method
: [get]

L’ajout de cette route permet de créer de « joli » liens sans le point d’interrogation et également de s’assurer que le le paramètre get fourni dans l’url est bien un entier positif (sinon erreur 404). Pour s’assurer également qu’un utilisateur ne va pas s’amuser à mettre le numéro d’une page qui n’existe pas, on peut rajouter le code suivant dans l’action index :

$this->forward404If($request->getParameter('page') > $this->pager->getLastPage());

Voilà, la prochaine étape sera de gérer la pagination de la liste en prenant en compte les paramètres de recherche d’un utilisateur

3 Commentaires sur “La pagination avec Symfony et Doctrine”

  • zip
    # Le 3 décembre 2011 à 11 h 03 min

    Chez Free,la dernière version de WordPress que l’on peut installer est WP 3.1.4 !

    1
  • mika
    # Le 4 décembre 2011 à 14 h 58 min

    @zip: ok merci pour l’info, mais quel rapport avec le sujet ?

    2
  • Marie
    # Le 9 février 2012 à 19 h 43 min

    Merci pour ce post qui va m’ être très utile.

    Et @Zip et @Mika merci pour cette conversation qui m’ a fait beaucoup rire !

    3

Laisser un commentaire

Vous pouvez ces balises HTML dans votre commentaire :
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Current month ye@r day *