IncludeBeer

Création d'une application web de base avec CodeIgniter 4 (4e partie)

Publié le 13 décembre 2020 Dernière mise à jour le 28 décembre 2020
Image de l'article
Origine de l'image: Bram Naus

Dans la troisième partie nous avons amélioré la présentation en affichant chaque recette sur une page séparée. Maintenant, nous allons l'améliorer un peu plus en ajoutant la pagination de la liste de recette.

Avant de commencer, ajoutez le champ slug à la liste de champs modifiables dans le modèle RecetteModel :

app/Models/RecetteModel.php

<?php namespace App\Models;

use CodeIgniter\Model;
use App\Entities\Recette;

class RecetteModel extends Model
{
    protected $table = 'recette';
    protected $returnType = Recette::Class;

    protected $allowedFields = [
        'titre',
        'instructions',
        'slug',
    ];
}

Vous pouvez aussi supprimer la Liste de recettes par id dans la vue liste_recettes pour ne garder que la Liste de recettes par slug. Supprimer les lignes suivantes :

    <h3>Liste des recettes par id</h3>
    <ul>
<?php foreach ($recettes as $recette): ?>
          <li><?= anchor('recette/' . $recette->id, $recette->titre) ?></li>
<?php endforeach; ?>
    </ul>

Insérer des données bidon avec la classe Seeder

Pour tester la pagination, ça va nous prendre beaucoup plus de données que ce qu'on a inséré dans les parties précédantes de cette série d'articles. Pour nous faciliter la vie, nous allons utiliser la classe Seeder. Cette classe nous permet de créer des données à partir de la ligne de commande (CLI). La classe RecettesSeeder est très simple, on obtient une instance du Query Builder pour chacune des tables recette et ingredient, on insère 500 nouvelles recettes avec le titre "Recette bidon numéro X", et pour chaque recette on insère trois ingrédients A, B et C en une seule étape avec la fonction insertBatch().

Créez la classe RecettesSeeder dans le répertoire app/Database/Seeds :

app/Database/Seeds/RecettesSeeder.php

<?php namespace App\Database\Seeds;

use CodeIgniter\Database\Seeder;

class RecettesSeeder extends Seeder
{
    public function run()
    {
        // Obtenir une instance du Query Builder pour la table RECETTE
        $qb_recette = $this->db->table('recette');

        // Obtenir une instance du Query Builder pour la table INGREDIENT
        $qb_ingredient = $this->db->table('ingredient');

        // Nombre de recettes bidons à créer
        $nb_recette = 500;

        // Boucler 500 fois
        for ($no_recette = 1; $no_recette <= $nb_recette; $no_recette++)
        {
            // Définir une recette bidon
            $recette = [
                'titre'  => "Recette bidon numéro {$no_recette}",
                'slug' => "recette-bidon-no-{$no_recette}",
                'instructions' => <<<EOT
Ajouter tous les ingrédients dans un plat.
Faire cuire au four à 350 °F (180 °C) pendant 45 minutes.
EOT
            ];

            // Insérer cette recette
            $qb_recette->insert($recette);

            // Obtenir l'ID de la recette créée
            $id_recette = $this->db->insertID();
            log_message('debug', "ID inséré: $id_recette");

            // Définir 3 ingrédients bidons associés à cette recette
            $ingredients = [
                [
                    'nom' => "Ingrédient A",
                    'quantite'  => "200 g",
                    'id_recette'  => $id_recette
                ],
                [
                    'nom' => "Ingrédient B",
                    'quantite'  => "50 g",
                    'id_recette'  => $id_recette
                ],
                [
                    'nom' => "Ingrédient C",
                    'quantite'  => "25 g",
                    'id_recette'  => $id_recette
                ]
            ];

            // Insérer ces 3 ingrédients
            $qb_ingredient->insertBatch($ingredients);
        }
    }
}

Exécuter une classe Seeder

La commande pour exécuter un "seeder" est la suivante :

php spark db:seed RecettesSeeder

Cette commande doit être exécutée à l'emplacement du script spark. Normalement, il est dans le répertoire racine de l'installation de CodeIgniter. C'est-à-dire un niveau au-desssu des répertoires app et system. Dans mon cas, j'utilise MAMP sur macOS et mon application est installée dans le répertoire ci4.test. Aussi, je n'utilise pas le PHP installé globalement sur mon ordinateur. J'utilise la version 7.4.2 qui est installée dans MAMP. Donc il suffit d'ouvrir une fenêtre Terminal et tapper les commandes suivantes :

cd /Applications/MAMP/htdocs/ci4.test
/Applications/MAMP/bin/php/php7.4.2/bin/php spark db:seed RecettesSeeder

L'exécution est très rapide, 2000 rangées sont insérées en moins de trois secondes (500 recettes + 1500 ingrédients). Vous pouvez maintenant rafaîchir la page http://ci4.test:8888 qui affichera maintenant une liste de 500 recettes, ou plus si vous avez gardez les recettes insérées dans la deuxième partie.

La pagination

Ok, une liste de 500 items ça n'a pas d'allure. C'est le temps d'implémenter la pagination !

La librairie MesRecettes

La façon la plus simple de paginer les résultats est d'utiliser la fonction paginate() de la classe Model à la place de la fonction findAll(). Cette fonction fait une bonne partie du travail à notre place. Elle va automatiquement lire la variable du numéro de page passée dans l'URL (?page=1), va configurer la classe Pager et va ajouter la clause LIMIT à la requête SQL, pour ensuite retourner le résultat final obtenu par findAll(). Pour utiliser la pagination, le contrôleur a besoin d'accéder au modèle car on doit utiliser l'instance du Pager qu'il a utilisé pour préparer la pagination. On va donc modifier notre librairie MesRecettes pour rendre les instances des modèles accessible via des variables publiques.

app/Libraries/MesRecettes.php

<?php namespace App\Libraries;

use App\Models\RecetteModel;
use App\Models\IngredientModel;

class MesRecettes
{
    public $recetteModel;
    public $ingredientModel;

    public function __construct()
    {
        $this->recetteModel = new RecetteModel();
        $this->ingredientModel = new IngredientModel();
    }

    /**
     * Obtenir la liste de recettes
     * @return array
     */
    public function getListeRecettes ()
    {
        // Obtenir seulement les champs id, slug et titre
        $this->recetteModel->select('id, slug, titre');

        // Ajouter le tri et la pagination, puis retourner les résultats
        $recettes = $this->recetteModel
            ->orderBy('id')
            ->paginate();

        return $recettes;
    }

    /**
     * Obtenir une recette par son id
     * @param int $id
     * @return object|NULL
     */
    public function getRecetteParId (int $id)
    {
        // Obtenir la recette par son id
        $recette = $this->recetteModel->find($id);

        if ($recette !== null)
        {
            $recette->ingredients = $this->ingredientModel
                ->where( ['id_recette' => $recette->id] )
                ->orderBy('id')
                ->findAll();
        }

        return $recette;
    }

    /**
     * Obtenir une recette par son 'slug'
     * @param string $slug
     * @return object|NULL
     */
    public function getRecetteParSlug (string $slug)
    {
        // Obtenir la recette par son slug
        $recette = $this->recetteModel->where('slug', $slug)->first();

        if ($recette !== null)
        {
            $recette->ingredients = $this->ingredientModel
                ->where( ['id_recette' => $recette->id] )
                ->orderBy('id')
                ->findAll();
        }

        return $recette;
    }
}

La seule chose à changer dans le contrôleur est d'obtenir l'instance de la classe Pager utilisée par le modèle et la passer à la vue. Ceci va nous permettre d'afficher les liens vers les autres pages.

app/Controllers/RecettesController.php

<?php namespace App\Controllers;

use App\Libraries\MesRecettes;

class RecettesController extends BaseController
{
    /**
     * Liste des recettes
     * @return string
     */
    public function index()
    {
        // Créer une instance de notre librairie
        $mesRecettes = new MesRecettes();

        // Rassembler toutes les données utilisées par la vue dans un tableau $data
        $data = [
            'titre_page' => "Mes recettes",
            'sous_titre_page' => "Je vous présente mes recettes favorites...",
            'recettes' => $mesRecettes->getListeRecettes(),
            // Passer l'instance de la classe de pagination à la vue
            'pager' => $mesRecettes->recetteModel->pager,
        ];

        /* Chacun des items du tableau $data sera accessible dans la vue
         * par des variables portant le même nom que la clé :
         * $titre_page, $sous_titre_page, $recettes, $rech et $pager
         */
        return view('liste_recettes', $data);
    }

    /**
     * Une seule recette
     * @param int $id
     * @return string
     */
    public function recetteParId (int $id)
    {
        // Créer une instance de notre librairie
        $mesRecettes = new MesRecettes();

        $data = [];

        /* Obtenir la recette pour l'id reçu en paramètre.
         * Si la recette n'existe pas, on emet une exception de page non trouvée (erreur 404)
         */
        if ( ! $data['recette'] = $mesRecettes->getRecetteParId($id))
        {
            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
        }

        return view('recette', $data);
    }

    /**
     * Une seule recette
     * @param string $slug
     * @return string
     */
    public function recetteParSlug (string $slug)
    {
        // Créer une instance de notre librairie
        $mesRecettes = new MesRecettes();

        $data = [];

        /* Obtenir la recette pour l'id reçu en paramètre.
         * Si la recette n'existe pas, on emet une exception de page non trouvée (erreur 404)
         */
        if ( ! $data['recette'] = $mesRecettes->getRecetteParSlug($slug))
        {
            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
        }

        return view('recette', $data);
    }
}

Configuration

La classe Pager est une classe système de CodeIgniter qui nous permet de faire la pagination et entre autre de générer les liens vers les autrs pages. Elle a besoin d'un fichier de configuration pour définir les templates disponibles et le nombre d'items par défaut à afficher par page. Ici nous ajoutons un nouveau template que nous allons utiliser à la place des templates par défaut de CodeIgniter. Les templates par défaut sont très basiques et au lieu d'écrire du CSS nous allons utiliser les classes CSS de Bootstrap.

app/Config/Pager.php

<?php namespace Config;

use CodeIgniter\Config\BaseConfig;

class Pager extends BaseConfig
{
    // Alias des templates pour la pagination
    public $templates = [
        // Templates par défaut fournit avec CodeIgniter
        'default_full'   => 'CodeIgniter\Pager\Views\default_full',
        'default_simple' => 'CodeIgniter\Pager\Views\default_simple',
        // Template personnalisé pour utiliser Bootstrap
        'bootstrap'      => 'App\Views\pagination_bootstrap',
    ];

    // Valeur par défaut pour le nombre d'items par page
    public $perPage = 25;
}

Template Bootstrap pour la pagination

Notre template personalisé n'est pas très différent du template original. On ajoute les classes CSS page-item et page-link, et on ajoute des liens vers la page précédente et la page suivante grâce aux fonctions getPreviousPage() et getNextPage().

app/Views/pagination_bootstrap.php

<?php
/**
 * @var \CodeIgniter\Pager\PagerRenderer $pager  Instance de la classe de pagination
 * @var array $link                              Information d'un lien vers une page (créée par l'instruction foreach)
 */

// Définir combien de numéro de page à afficher de chaque côté du numéro de la page courante
$pager->setSurroundCount(2);
?>

<nav aria-label="<?= lang('Pager.pageNavigation') ?>">
    <ul class="pagination justify-content-center">
    <?php if ($pager->hasPrevious()) : ?>
        <li class="page-item">
            <a href="<?= $pager->getFirst() ?>" aria-label="<?= lang('Pager.first') ?>" class="page-link">
                <span aria-hidden="true"><?= lang('Pager.first') ?></span>
            </a>
        </li>
        <li>
            <a href="<?= $pager->getPreviousPage() ?>" aria-label="<?= lang('Pager.previous') ?>" class="page-link">
                <span aria-hidden="true"><?= lang('Pager.previous') ?></span>
            </a>
        </li>
        <li class="page-item">
            <a href="<?= $pager->getPrevious() ?>" class="page-link">
                <span aria-hidden="true">&laquo;</span>
            </a>
        </li>
    <?php endif ?>

    <?php foreach ($pager->links() as $link) : ?>
      <li class="<?= $link['active'] ? 'page-item active' : 'page-item' ?>">
            <a href="<?= $link['uri'] ?>" class="page-link">
                <?= $link['title'] ?>
            </a>
        </li>
    <?php endforeach ?>

    <?php if ($pager->hasNext()) : ?>
        <li class="page-item">
            <a href="<?= $pager->getNext() ?>" class="page-link">
                <span aria-hidden="true">&raquo;</span>
            </a>
        </li>
        <li>
          <a href="<?= $pager->getNextPage() ?>" aria-label="<?= lang('Pager.next') ?>" class="page-link">
              <span aria-hidden="true"><?= lang('Pager.next') ?></span>
            </a>
        </li>
        <li class="page-item">
            <a href="<?= $pager->getLast() ?>" aria-label="<?= lang('Pager.last') ?>" class="page-link">
                <span aria-hidden="true"><?= lang('Pager.last') ?></span>
            </a>
        </li>
    <?php endif ?>
    </ul>
</nav>

La vue liste_recettes

Maintenant il ne reste qu'à afficher les liens vers les autres pages avec la fonction $pager->links(). Par contre, ceci utilise le template par défaut de CodeIgniter. Nous allons donc appeler la fonction en lui spécifiant d'utiliser notre template qu'on a nommé bootstrap dans le fichier de configuration. On aurait pu lui donner n'importe quel autre nom. Le premier paramètre sert à définir le groupe de pagination dans les cas ou on aurait plusieurs listes à paginer sur la même page. Comme on a seulement un ensemble de résultats à paginer, on passe 'default' :

<?= $pager->links('default', 'bootstrap') ?>

app/Views/liste_recettes.php

<?php
/**
 * @var string $titre_page                       Le titre de la page (créée automatiquement par CI via le tableau $data)
 * @var string $sous_titre_page                  Le sous-titre de la page (créée automatiquement par CI via le tableau $data)
 * @var array $recettes                          Liste des recettes (créée automatiquement par CI via le tableau $data)
 * @var App\Entities\Recette $recette            Une recette (créée par l'instruction foreach)
 * @var \CodeIgniter\Pager\PagerRenderer $pager  Instance de la classe de pagination
 */
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<title><?= esc($titre_page) ?></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet"
    href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
    integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
    crossorigin="anonymous">

<style type="text/css">
.titre
{
    padding: 3rem 1.5rem;
}

article
{
    padding: 1.5rem 1.5rem;
}
</style>

</head>

<body>

<main role="main" class="container">
    <div class="titre">
        <h1>
          <?= esc($titre_page) ?>
          <small class="text-muted"><?= esc($sous_titre_page) ?></small>
      </h1>
    </div>

    <div class="container">

      <h3>Liste des recettes</h3>
    <ul>
<?php foreach ($recettes as $recette): ?>
          <li><?= anchor('recette/' . $recette->slug, $recette->titre) ?></li>
<?php endforeach; ?>
    </ul>

      <?= $pager->links('default', 'bootstrap') ?>

    </div>

</main>

<footer>
    <p class="text-center">&copy; 2020 Mon site de recettes</p>
</footer>

</body>
</html>

La vue recette

Dans la vue recette, j'ai ajouté un appel à la fonction nl2br() qui signifie "new line to break". C'est-à-dire que ça transforme les caractères de sauts de ligne \n en leur équivalent HTML : <br>. Ajoutez l'appel à la ligne où on affiche les instructions :

app/Views/recette.php

<h5>Préparation</h5>
<p><?= nl2br( esc($recette->instructions) ) ?></p>

Quatrième partie terminée !

Vous pouvez maintenant rafaîchir la page http://ci4.test:8888 qui affichera maintenant 25 recettes par page avec des liens pour aller aux pages suivantes et précédentes, première et dernière page, les numéros de pages et des liens pour avancer ou reculer de plusieurs pages. Maintenant qu'on voit de quoi a l'air l'application avec une tonne de recettes réparties sur plusieurs pages, c'est évident que ça va prendre un formulaire de recherche. C'est ce que nous allons faire dès maintenant dans la cinquième partie.