• 50 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

Ce cours est en vidéo.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

J'ai tout compris !

Mis à jour le 27/02/2019

Tutoriel - Paginez une liste de ressources

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Pour faire en sorte que notre API soit en mesure de lister l'ensemble des articles que l'application gère, il suffit simplement d'ajouter une méthode de controller (une action), comme suit :

<?php

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use FOS\RestBundle\Controller\Annotations as Rest;

class ArticleController extends Controller
{
    /**
     * @Rest\Get("/articles", name="app_article_list")
     */
    public function listAction()
    {
        $articles = $this->getDoctrine()->getRepository('AppBundle:Article')->findAll();
        
        return $articles;
    }
}
Liste d'articles
Liste d'articles

Néanmoins, le nombre d'articles peut atteindre un nombre important ! Il faut donc paginer cette liste.

Je vous propose donc de suivre ce petit tutoriel, où je vous montre pas à pas comment y arriver.

Installer la librairie PagerFanta

Nous allons utiliser la librairie PagerFanta pour faciliter notre travail. Nous allons passer par le bundle WhiteOctoberPagerfantaBundle car il intègre la librairie et nous facilite son utilisation avec Symfony.

Pour l'installer, tapez la commande suivante :

$ composer require white-october/pagerfanta-bundle

Installation du PagerFantaBundle
Installation du PagerFantaBundle

Il nous faut maintenant le déclarer dans la classeAppKernel:

<?php

// …

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = [
            // …
            new WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle(),
        ];

        // …

        return $bundles;
    }
    // …
}

Premiers pas avec la librairie

La librairie nous fournit un ensemble de méthodes pour nous faciliter la vie ! Prenons un exemple pour découvrir ensemble ces méthodes avant de développer ce dont nous avons besoin pour notre liste d'articles à paginer.

Disons que nous avons des éléments contenus dans un tableau$elements. Il faut, dans un premier temps, créer ce que nous appelons un adapter :

<?php

// …

$adapter = new \Pagerfanta\Adapter\ArrayAdapter($elements);
$pager = new \Pagerfanta\Pagerfanta($adapter);

La variable  $elements  contient l'ensemble des éléments à paginer. Il s'agit d'un tableau, il faut donc utiliser l'ArrayAdapter. Une fois l'adapter instancié, il suffit de créer lepager(ligne 6).

Voici la liste des méthodes intéressantes que nous pourrons utiliser :

<?php

//…

$pager->setMaxPerPage($maxPerPage); // Enregistrer le nombre d'éléments à avoir au maximum dans une page
$pager->getMaxPerPage(); // Récupérer le nombre d'éléments qu'il peut y avoir au maximum dans une page

$pager->setCurrentPage($currentPage); // Enregister la page courante
$pager->getCurrentPage(); // Numéro de la page courante

$pager->getNbResults(); // Nombre de résultats dans la page courante

$pager->getNbResults(); // Nombre total de résultats
$pager->getCurrentPageResults(); // Éléments de la page courante

$pager->haveToPaginate(); // Retourne true si le nombre de résultats est supérieur au nombre maximum d'éléments par page

$pager->getNbPages(); // Nombre total de pages

$pager->hasPreviousPage(); // Retourne true si la page courante possède une page précédente

$pager->getPreviousPage(); // Numéro de la page précédente

$pager->hasNextPage(); // Retourne true si la page courante possède une page suivante

$pager->getNextPage(); // Numéro de la page suivante

Pagination d'éléments récupérés via Doctrine

Dans notre cas, nous avons besoin de récupérer nos éléments via Doctrine. Dans le cas où nous avons de plus en plus d'articles, récupérer tous les articles, puis hydrater des dizaines de milliers d'objets qui peuvent eux-mêmes avoir des relations vers d'autres objets… et tout cela en mémoire ! :waw: Vous voyez le problème évident que cela pose. Il faut prendre l'habitude de paginer vos résultats. Nous allons donc faire appel à l'adapter prévu pour Doctrine afin de créer un pager.

Dans la mesure où, dans la majorité des cas, vous aurez sans doute plusieurs types d'éléments à paginer, en plus des articles (des utilisateurs, des produits… et que sais-je encore), nous allons créer une classe abstraite reprenant le code que vous serez en mesure de réutiliser pour paginer n'importe quel élément.

Par ailleurs, nous allons faire en sorte qu'il soit possible que l'utilisateur de l'API puisse rechercher parmi les titres des articles.

Allez c'est parti ! Listons d'abord les étapes pour y arriver :

  1. Classe de repository abstraite permettant de paginer les résultats (implémentation d'une méthode  paginate())

  2. Utilisation de la méthode paginate() définie dans la classe abstraite du repository afin de spécifier la requête pour retrouver les articles par leur titre

  3. Méthode de Controller pour récupérer le(s) terme(s) de recherche, le nombre maximum d'éléments par page

  4. Créer une représentation de la collection, un objet capable d'englober toutes les informations inhérente à une liste

Etape 1 : classe abstraite de repository permettant de paginer les résultats

Créons la classe abstraite  AbstractRepository :

<?php

namespace AppBundle\Repository;

use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use PagerFanta\Adapater\DoctrineORMAdapter;
use PagerFanta\Pagerfanta;

abstract class AbstractRepository extends EntityRepository
{
    protected function paginate(QueryBuilder $qb, $limit = 20, $offset = 0)
    {
        if (0 == $limit || 0 == $offset) {
            throw new \LogicException('$limit & $offstet must be greater than 0.');
        }
        
        $pager = new Pagerfanta(new DoctrineORMAdapter($qb));
        $currentPage = ceil(($offset + 1) / $limit);
        $pager->setCurrentPage($currentPage);
        $pager->setMaxPerPage((int) $limit);
        
        return $pager;
    }
}

Cette méthode prend en paramètre les arguments :

  •  $limit : le nombre maximum d'éléments par page ;

  •  $offset : l'index de l'élément par lequel on commence ;

  •  $qb : le query builder qui contient le début de la requête permettant de rechercher les éléments à paginer.

Étape 2 : classe de repository avec la méthode permettant d'aller chercher tous les éléments que l'on souhaite paginer

Maintenant, voyons comment utiliser la méthode paginate() de la classe abstraite dans le repository de l'article :

<?php

namespace AppBundle\Repository;

class ArticleRepository extends AbstractRepository
{
    public function search($term, $order = 'asc', $limit = 20, $offset = 0)
    {
        $qb = $this
            ->createQueryBuilder('a')
            ->select('a')
            ->orderBy('a.title', $order)
        ;
        
        if ($term) {
            $qb
                ->where('a.title LIKE ?1')
                ->setParameter(1, '%'.$term.'%')
            ;
        }
        
        return $this->paginate($qb, $limit, $offset);
    }
}

 Il nous faut également déclarer la classe comme repository de l'entitéArticle:

<?php

namespace AppBundle\Entity;

// …

/**
 * @ORM\Entity(repositoryClass="AppBundle\Repository\ArticleRepository")
 * …
 *
 */
class Article
{
    //…

Étape 3 : récupérer les informations en rapport avec le nombre d'éléments maximum à récupérer par page

Appelons maintenant la méthode  search   du repository dans la méthode de controller pour récupérer les éléments en fonction des termes demandés par le client et l'index du dernier élément affiché précédemment.

Mettons à jour la méthode listAction() de notre controller :

<?php

namespace AppBundle\Controller;

// …
use FOS\RestBundle\Request\ParamFetcherInterface;

class ArticleController extends Controller
{
        /**
     * @Rest\Get("/articles", name="app_article_list")
     * @Rest\QueryParam(
     *     name="keyword",
     *     requirements="[a-zA-Z0-9]",
     *     nullable=true,
     *     description="The keyword to search for."
     * )
     * @Rest\QueryParam(
     *     name="order",
     *     requirements="asc|desc",
     *     default="asc",
     *     description="Sort order (asc or desc)"
     * )
     * @Rest\QueryParam(
     *     name="limit",
     *     requirements="\d+",
     *     default="15",
     *     description="Max number of movies per page."
     * )
     * @Rest\QueryParam(
     *     name="offset",
     *     requirements="\d+",
     *     default="0",
     *     description="The pagination offset"
     * )
     * @Rest\View()
     */
    public function listAction(ParamFetcherInterface $paramFetcher)
    {
        $pager = $this->getDoctrine()->getRepository('AppBundle:Article')->search(
            $paramFetcher->get('keyword'),
            $paramFetcher->get('order'),
            $paramFetcher->get('limit'),
            $paramFetcher->get('offset')
        );

        return $pager->getCurrentPageResults();
    }
    
    // …
}

Si nous testons notre action, nous pouvons voir que la sérialisation ne se passe pas comme prévue :

Résultat
Résultat

Il nous faut formater un peu le résultat afin que la sérialisation se passe mieux. Profitons-en pour y incorporer toutes les informations que nous pouvons obtenir avec le pager. Nous allons passer par une classe intermédiaire, Articles. Créons la classeArticlesdans le dossier AppBundle/Representation :

<?php

namespace AppBundle\Representation;

use Pagerfanta\Pagerfanta;

class Articles
{
    public $data;
    public $meta;
    
    public function __construct(Pagerfanta $data)
    {
        $this->data = $data;
        
        $this->addMeta('limit', $data->getMaxPerPage());
        $this->addMeta('current_items', count($data->getCurrentPageResults()));
        $this->addMeta('total_items', $data->getNbResults());
        $this->addMeta('offset', $data->getCurrentPageOffsetStart());
    }
    
    public function addMeta($name, $value)
    {
        if (isset($this->meta[$name])) {
            throw new \LogicException(sprintf('This meta already exists. You are trying to override this meta, use the setMeta method instead for the %s meta.', $name));
        }
        
        $this->setMeta($name, $value);
    }
    
    public function setMeta($name, $value)
    {
        $this->meta[$name] = $value;
    }
}

Il nous faut ajouter un peu de configuration afin que la sérialisation se déroule correctement. Commençons avec la classe  AppBundle\Entity\Article. Il nous faut appliquer la stratégie d'exclusion "all" et n'exposer que les champs qui nous intéressent :

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;

/**
 * @ORM\Entity(repositoryClass="AppBundle\Repository\ArticleRepository")
 * @ORM\Table()
 *
 * @ExclusionPolicy("all")
 *
 */
class Article
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     *
     * @Expose
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=100)
     * @Expose
     */
    private $title;

    /**
     * @ORM\Column(type="text")
     * @Expose
     */
    private $content;
    
    // …
}

 Il ne nous reste plus qu'à configurer la sérialisation pour les champs de la classe  AppBundle\Representation\Articles  :

<?php

namespace AppBundle\Representation;

use Pagerfanta\Pagerfanta;
use JMS\Serializer\Annotation\Type;

class Articles
{
    /**
     * @Type("array<AppBundle\Entity\Article>")
     */
    public $data;

    public $meta;

    public function __construct(Pagerfanta $data)
    {
        $this->data = $data->getCurrentPageResults();

        $this->addMeta('limit', $data->getMaxPerPage());
        $this->addMeta('current_items', count($data->getCurrentPageResults()));
        $this->addMeta('total_items', $data->getNbResults());
        $this->addMeta('offset', $data->getCurrentPageOffsetStart());
    }

    public function addMeta($name, $value)
    {
        if (isset($this->meta[$name])) {
            throw new \LogicException(sprintf('This meta already exists. You are trying to override this meta, use the setMeta method instead for the %s meta.', $name));
        }

        $this->setMeta($name, $value);
    }

    public function setMeta($name, $value)
    {
        $this->meta[$name] = $value;
    }
}

Il ne nous reste plus qu'à mettre à jour le controller :

<?php

namespace AppBundle\Controller;

// …
use AppBundle\Representation\Articles;

class ArticleController extends Controller
{
    /**
     * …
     */
    public function listAction(ParamFetcherInterface $paramFetcher)
    {
        // …

        return new Articles($pager);
    }
    
    // …
}

Et voilà !

Liste d'articles
Liste d'articles

Rendez-vous au prochain chapitre pour apprendre à valider une ressource.

Exemple de certificat de réussite
Exemple de certificat de réussite