8. Mettre en place une pagination

 Afin d'optimiser la récupération de nos ressources, mettons en place une pagination sur la route qui va lister nos ressources.

 

Modifions notre PhoneController

Commençons par travailler dans notre fichier PhoneController. Ici, on va avoir besoin de récupérer la requête pour savoir si un numéro de page est donné.

On va aussi avoir besoin de passer des paramètres à notre méthode.

Enfin, on va devoir fixer une limite à notre pagination, elle sera composée de la page demandée (récupérée par la requête), et aussi du nombre de ressource maximum que l'on veut récupérer.

Je vais fixer ici le nombre à 10, mais libre à vous de choisir la quantité souhaitée.

Voici notre contrôleur actualisé : 

<?php

namespace App\Controller;

use App\Repository\PhoneRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface;

/**
 * @Route("/api/phones")
 */
class PhoneController extends AbstractController
{
    /**
     * @Route("/{page<\d+>?1}", name="list_phone", methods={"GET"})
     */
    public function index(Request $request, PhoneRepository $phoneRepository, SerializerInterface $serializer)
    {
        $page = $request->query->get('page');
        $limit = 10;
        $phones = $phoneRepository->findAllPhones($page, $limit);
        $data = $serializer->serialize($phones, 'json');
        return new Response($data, 200, [
            'Content-Type' => 'application/json'
        ]);
    }
}

Le numéro de la page est récupéré dans le paramètre de route, la valeur par défaut étant à 1 si aucune page n'est précisée.

On récupère le numéro de la page dans la variable $page. La limité à été fixée à 10 dans la variable $limit.

Ces deux valeurs sont passées à la méthode findAllPhones pour récupérer nos données.

Cette méthode n'existe pas dans notre repository, créons-là dès maintenant 😉

 

Ajoutons une méthode dans le PhoneRepository

Modifions maintenant notre PhoneRepository en ajoutant notre nouvelle méthode :

<?php

namespace App\Repository;

use App\Entity\Phone;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Symfony\Bridge\Doctrine\RegistryInterface;

/**
 * @method Phone|null find($id, $lockMode = null, $lockVersion = null)
 * @method Phone|null findOneBy(array $criteria, array $orderBy = null)
 * @method Phone[]    findAll()
 * @method Phone[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class PhoneRepository extends ServiceEntityRepository
{
    public function __construct(RegistryInterface $registry)
    {
        parent::__construct($registry, Phone::class);
    }

    public function findAllPhones($page, $limit)
    {
        $query = $this->createQueryBuilder('p')
            ->getQuery()
            ->setFirstResult(($page - 1) * $limit)
            ->setMaxResults($limit);
        return new Paginator($query);
    }
}

Ici, les deux valeurs passées en paramètres sont utilisées pour renvoyer le nombre exact de ressources demandées selon la page donnée.

Retournez sur Postman, allez pointer sur l'URL http://localhost:8000/api/phones?page=1 et admirez le résultat :


 

On récupère bien nos 10 ressources 😊

Essayez d'accéder à la page 2 et admirez le résultat : 


 

Eh mais attends, si j'accède à http://localhost:8000/api/phones, ça ne marche pas, c'est un peu dommage ça non ?


 

Oui, ce n'est pas idéal, corrigeons notre code : 

<?php

namespace App\Controller;

use App\Repository\PhoneRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface;

/**
 * @Route("/api/phones")
 */
class PhoneController extends AbstractController
{
    /**
     * @Route("/{page<\d+>?1}", name="list_phone", methods={"GET"})
     */
    public function index(Request $request, PhoneRepository $phoneRepository, SerializerInterface $serializer)
    {
        $page = $request->query->get('page');
        if(is_null($page) || $page < 1) {
            $page = 1;
        }
        $limit = 10;
        $phones = $phoneRepository->findAllPhones($page, $limit);
        $data = $serializer->serialize($phones, 'json');
        return new Response($data, 200, [
            'Content-Type' => 'application/json'
        ]);
    }
}

Actualisez Postman et admirez le résultat : 

 

Une pagination réutilisable ?

On vient ici de mettre en place une pagination pour notre Entité Phone, mais la question qu'on peut se poser est : Aurais-je besoin d'une autre pagination ailleurs ?

La réponse est... bonne question.

Tout dépend du besoin dans votre API et aussi de la quantité de ressources que vous souhaitez récupérer par page.

Dans notre API, on va ici stocker la limite dans une variable d'environnement, pour pouvoir la réutiliser au besoin.

Voici le contrôleur actualisé : 

<?php

namespace App\Controller;

use App\Repository\PhoneRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface;

/**
 * @Route("/api/phones")
 */
class PhoneController extends AbstractController
{
    /**
     * @Route("/{page<\d+>?1}", name="list_phone", methods={"GET"})
     */
    public function index(Request $request, PhoneRepository $phoneRepository, SerializerInterface $serializer)
    {
        $page = $request->query->get('page');
        if(is_null($page) || $page < 1) {
            $page = 1;
        }
        $phones = $phoneRepository->findAllPhones($page, getenv('LIMIT'));
        $data = $serializer->serialize($phones, 'json');
        return new Response($data, 200, [
            'Content-Type' => 'application/json'
        ]);
    }
}

N'oubliez pas d'ajouter votre variable LIMIT dans le fichier .env : 

# In all environments, the following files are loaded if they exist,
# the later taking precedence over the former:
#
#  * .env                contains default values for the environment variables needed by the app
#  * .env.local          uncommitted file with local overrides
#  * .env.$APP_ENV       committed environment-specific defaults
#  * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration

###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=82efa4bae098421ef28a597dd4407aae
#TRUSTED_PROXIES=127.0.0.1,127.0.0.2
#TRUSTED_HOSTS='^localhost|example\.com$'
###< symfony/framework-bundle ###

###> doctrine/doctrine-bundle ###
# Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db"
# Configure your db driver and server_version in config/packages/doctrine.yaml
DATABASE_URL=mysql://root:root@127.0.0.1:3306/api
###< doctrine/doctrine-bundle ###
LIMIT=10

Attention ici à ne pas mettre d'espace, sinon vous aurez une erreur qui s'affiche lorsque vous allez actualiser votre page.

Actualisez votre page, le résultat est le même.

Occupons-nous maintenant de récupérer une seule ressource.