IncludeBeer

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

Publié le 15 novembre 2020 Dernière mise à jour le 16 novembre 2020
Image de l'article
Origine de l'image: Bram Naus

Dans la deuxième partie nous avons créé une application web très simple avec une base de données. Maintenant, nous allons améliorer la présentation en affichant chaque recette sur une page séparée.

Mise à jour de la base de données

La façon la plus simple d'obtenir une rangée de la base de donnée, est de la chercher à l'aide de sa clé primaire, soit son id. Mais une url dans le genre de http://ci4.test/recette/1 ce n'est pas très intéressant. Si on voit simplement cette url, on n'a aucune idée de quelle recette il s'agit. Du point de vue SEO, ce n'est pas une bonne idée non plus. On va donc ajouter un champ slug qui va nous permettre d'avoir des url qui incluent le titre de la recette: http://ci4.test/recette/eau-bouillante.

Exécutez le script suivant pour ajouter la nouvelle colonne et la populer avec un titre qui peut être utilisé dans une url :

--
-- Table pour les recettes
--
ALTER TABLE `recette`
  ADD `slug` varchar(255);

--
-- Mettre à jour les recettes
--
UPDATE `recette`
SET `slug` = 'eau-bouillante'
WHERE `titre` = 'Eau bouillante';

UPDATE `recette`
SET `slug` = 'the'
WHERE `titre` = 'Thé';

UPDATE `recette`
SET `slug` = 'verre-d-eau'
WHERE `titre` = 'Verre d''eau';

La route

Pour afficher une recette sur une page séparée, il faut ajouter une nouvelle route vers la page d'une recette: recette/(:slug). Mais pour démontrer les deux façons de faire, on ajoute aussi une route vers la page d'une recette en utilisant son id : recette/(:num). Pour la route vers recette/(:slug), nous devons définir une regex qui accepte les caractères alpha-numériques séparés par des tirets. C'est la fonction $routes->addPlaceholder() qui nous permet de déinir le type (:slug). On pourrait lui donner n'importe quel autre nom, mais slug est le terme habituellement utilisé pour définir le titre d'un article dans une url. Voici la nouvelle définition des routes, incluant le nouveau placeholder 'slug', ainsi que les routes vers la liste de recettes et les deux routes vers une recette, par id et par slug.

app/Config/Routes.php

/**
 * --------------------------------------------------------------------
 * Route Definitions
 * --------------------------------------------------------------------
 */

/**
 * slug placeholder:
 *
 *  [a-z0-9]+     # Une ou plusieurs répétitions de ces caractères
 *  (?:           # Un groupe sans capture
 *    -           # Un tiret
 *    [a-z0-9]+   # Une ou plusieurs répétitions de ces caractères
 *  )*            # Aucune ou plusieurs répétitions du précédent groupe
 *
 *  Ceci va faire un match avec :
 *  - Une séquence de caractères alphanumériques au début de la chaîne de caractères.
 *  - Ensuite ça va trouver un tiret, puis une séquence de caractères alphanumériques,
 *    aucune ou plusieurs fois.
 *
 * Exemples :
 *   item12345
 *   un-article-de-blog
 *
 */
$routes->addPlaceholder('slug', '[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*');

// Définir la route /, recette/id et recette/slug
$routes->get('/', 'RecettesController::index');
$routes->get('recette/(:num)', 'RecettesController::recetteParId/$1');
$routes->get('recette/(:slug)', 'RecettesController::recetteParSlug/$1');

La librairie MesRecettes

Dans la librairie MesRecettes, on ajoute une nouvelle fonction getListeRecettes() pour obtenir la liste des recettes. Cette nouvelle fonction est maintenant utilisée à la place de getToutesLesRecettes(). Elle obtient le minimum d'information requis pour afficher la liste. Soit le id, le slug et le titre. On n'obtient pas les instructions ni la liste d'ingrédients. On ajoute aussi les fonctions getRecetteParId (int $id) et getRecetteParSlug (string $slug) pour obtenir une seule recette, soit par son id ou par son slug.

app/Libraries/MesRecettes.php

<?php namespace App\Libraries;

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

class MesRecettes
{
    /**
     * Obtenir toutes les recettes
     * @return array
     */
    public function getToutesLesRecettes ()
    {
        // Créer une instance de nos deux modèles
        $recetteModel = new RecetteModel();
        $ingredientModel = new IngredientModel();

        // Faire un SELECT des recettes, triées par id
        $recettes = $recetteModel
            ->orderBy('id')
            ->findAll();

        // Pour chaque recette, faire un SELECT de ses ingrédients
        foreach ($recettes as &$recette)
        {
            $recette->ingredients = $ingredientModel
                ->where( ['id_recette' => $recette->id] )
                ->orderBy('id')
                ->findAll();
        }
        unset($recette);

        return $recettes;
    }

    /**
     * Obtenir la liste de recettes
     * @return array
     */
    public function getListeRecettes ()
    {
        $recetteModel = new RecetteModel();

        // Obtenir seulement les champs id, slug et titre
        $recettes = $recetteModel
            ->select('id, slug, titre')
            ->orderBy('id')
            ->findAll();

        return $recettes;
    }

    /**
     * Obtenir une recette par son id
     * @param int $id
     * @return object|NULL
     */
    public function getRecetteParId (int $id)
    {
        $recetteModel = new RecetteModel();
        $ingredientModel = new IngredientModel();

        // Obtenir la recette par son id
        $recette = $recetteModel->find($id);

        if ($recette !== null)
        {
            $recette->ingredients = $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)
    {
        $recetteModel = new RecetteModel();
        $ingredientModel = new IngredientModel();

        // Obtenir la recette par son slug
        $recette = $recetteModel->where('slug', $slug)->first();

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

        return $recette;
    }
}

Le contrôleur RecettesController

Dans le contrôleur, la liste de recette est dorénavent obtenue grâce à la nouvelle fonction $mesRecettes->getListeRecettes(). On ajoute aussi deux nouvelles fonctions pour afficher une recette sur une page séparée en la cherchant par son id ou par son slug : recetteParId (int $id) et recetteParSlug (string $slug). Ces fonctions affichent la nouvelle vue recette.

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();

        $data = [
            'titre_page' => "Mes recettes",
            'sous_titre_page' => "Je vous présente mes recettes favorites...",
            'recettes' => $mesRecettes->getListeRecettes(),
        ];

        /* 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 et $recettes
         */
        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);
    }
}

La vue liste_recettes

Dans la vue liste_recettes, on fait quelques ajustements pour afficher la liste des recettes plutôt que les recettes complètes. On utilise la fonction anchor() pour générer un hyperlien vers la page de la recette. Cette fonction fait partie de l'ensemble de fonctions URL Helper et est chargée automatiquement par le framework. Pour démontrer les deux façons de faire, on génère une liste d'hyperliens qui obtiennent les recettes à partir de leur id, puis une liste qui les obtiens à partir de leur slug.

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)
 */
?>
<!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 par id</h3>
        <ul>
<?php foreach ($recettes as $recette): ?>
            <li><?= anchor('recette/' . $recette->id, $recette->titre) ?></li>
<?php endforeach; ?>
        </ul>

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

    </div>

</main>

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

</body>
</html>

La vue recette

La nouvelle vue recette affiche les informations complètes d'une recette, incluant les instructions et les ingrédiens.

app/Views/recette.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 App\Entities\Ingredient $ingredient  Un ingrédient (créée par l'instruction foreach)
 */
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<title><?= esc($recette->titre) ?></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($recette->titre) ?>
        </h1>
    </div>

    <div class="container">
        <article>
            <h5>Ingrédients</h5>
            <ul>
            <?php foreach ($recette->ingredients as $ingredient): ?>
                <li><?= esc($ingredient->quantite) ?> <?= esc($ingredient->nom) ?></li>
            <?php endforeach; ?>
            </ul>
            <h5>Préparation</h5>
            <p><?= esc($recette->instructions) ?></p>
        </article>
    </div>

</main>

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

</body>
</html>

Troisième partie terminée !

Vous pouvez maintenant rafaîchir la page http://ci4.test:8888 qui affichera maintenant la liste des titres des recettes. Un clic sur une recette affiche une nouvelle page avec toutes les informations de la recette. On peut accéder à une recette par son id ou par son slug.