IncludeBeer

Création d'une application web de base avec CodeIgniter 4 (2e 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 première partie nous avons créé une application web très simple avec du contenu statique. Maintenant, ajoutons un peu de dynamisme en y intégrant une base de données MySQL.

Création de la base de données

CodeIgniter offre la possibilité de manipuler la base de données avec les classes Migration et Seeder, mais pour garder cet exemple le plus simple possible, nous allons exécuter les requêtes SQL directement dans MySQL. Créez une nouvelle base de données avec votre logiciel préféré, tel que PhpMyAdmin, DBeaver, mysql en ligne de commande ou n'importe quoi d'autre. Ensuite, exécutez les requêtes suivantes pour créer les tables et y insérer les données exemples.

La table recette contient les recettes : le id, le titre de la recette et ses instructions. Comme une recette contient plusieurs ingrédients, on crée une table ingredient qui contient la liste des ingrédients de toutes les recettes : le id, le nom de l'ingrédient, la quantité requise et le id de la recette auquel il appartient. On ajoute aussi une contrainte de clé étrangère (foreign key) pour garantir l'intégrité entre les deux tables. Avec cette contrainte, on est certains que la colonne id_recette pointe vers une recette existante.

Exécutez les requêtes suivantes dans votre logiciel de base de données favoris pour créer les tables :

--
-- Table pour les recettes
--
CREATE TABLE `recette` (
  `id` int(11) NOT NULL,
  `titre` varchar(100) NOT NULL,
  `instructions` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

ALTER TABLE `recette`
  ADD PRIMARY KEY (`id`);

ALTER TABLE `recette`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;

--
-- Table pour les ingrédients
--
CREATE TABLE `ingredient` (
  `id` int(11) NOT NULL,
  `id_recette` int(11) NOT NULL,
  `nom` varchar(50) NOT NULL,
  `quantite` varchar(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

ALTER TABLE `ingredient`
  ADD PRIMARY KEY (`id`),
  ADD KEY `recette_fk` (`id_recette`);

ALTER TABLE `ingredient`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;

--
-- Contrainte de clé étrangère (foreign key)
--
ALTER TABLE `ingredient`
  ADD CONSTRAINT `recette_fk` FOREIGN KEY (`id_recette`) REFERENCES `recette` (`id`);

Insérer les données dans la base de données

Exécutez les requêtes suivantes pour ajouter les recettes et leurs ingrédients dans les tables qu'on vient de créer. Évidemment vous pouvez ajouter des vraies recettes au lieu de ces exemples un peu niaiseux :

--
-- Insérer les recettes
-- Le id des recettes est généré automatiquement par MySQL
--
INSERT INTO `recette`
  (`titre`,
   `instructions`)
VALUES
  ('Eau bouillante',
   'Mettre l''eau dans un chaudron et faire bouillir.'),
  ('Thé',
   'Préparez la recette d''eau bouillante. Mettre l''eau dans une tasse, ajoutez la poche de thé et laissez infuser quelques minutes.'),
  ('Verre d''eau',
   'Mettre des glaçons dans un grand verre et remplir d''eau. Ajoutez une tranche de citron si désiré.');

--
-- Insérer les ingrédients pour chaque recette
-- Le id des ingrédient est généré automatiquement par MySQL
-- mais on sélectionne le id de la recette correspondante en faisant une recherche par nom
--
INSERT INTO `ingredient`
  (`id_recette`,
   `nom`,
   `quantite`)
VALUES
  ((SELECT `id` FROM `recette` WHERE `titre` = 'Eau bouillante'),
   'Eau fraîche',
   '250 ml'),
  ((SELECT `id` FROM `recette` WHERE `titre` = 'Thé'),
   'Eau fraîche',
   '250 ml'),
  ((SELECT `id` FROM `recette` WHERE `titre` = 'Thé'),
   'Poche de thé',
   '1'),
  ((SELECT `id` FROM `recette` WHERE `titre` = 'Verre d''eau'),
   'Eau fraîche',
   '300 ml'),
  ((SELECT `id` FROM `recette` WHERE `titre` = 'Verre d''eau'),
   'Glaçon',
   '2-3'),
  ((SELECT `id` FROM `recette` WHERE `titre` = 'Verre d''eau'),
   'Citron (facultatif)',
   '1 tranche');

app/Config/Database.php

Configuration de la connexion à la base de données. Ajustez les valeurs selon votre configuration :

/**
 * The default database connection.
 *
 * @var array
 */
public $default = [
    'DSN'      => '',

    // Définir le hostname. 
    // Si MySQL est installé sur votre ordinateur, localhost devrait être bon.
    'hostname' => 'localhost',

    // Définir le username et password. 
    // Le compte par défaut de MySQL est root/root
    'username' => 'root',
    'password' => 'root',

    // Définir le nom de la base de données. 
    // Je l'ai nommée ci4 mais ça peut être n'importe quoi.
    'database' => 'ci4',

Les modèles RecetteModel et IngredientModel

La classe Model de CodeIgniter 4 offre beaucoup de fonctionnalités. Dans sa forme la plus simple, il suffit de définir le nom de la table, le type d'objet à retourner et la liste des champs modifiables. Dans cet exemple, on permet de modifier tous les champs, sauf les id. Comme on n'a pas encore de formulaire pour éditer une recette, on pourrait tout simplement laisser le champ vide et tout les champs seraient non modifiables.

app/Models/RecetteModel.php

<?php namespace App\Models;

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

class RecetteModel extends Model
{
    // Le nom de la table MySQL
    protected $table = 'recette';

    // Le type d'objet à retourner
    protected $returnType = Recette::Class;

    // Les champs modifiables
    protected $allowedFields = [
        'titre',
        'instructions',
    ];
}

app/Models/IngredientModel.php

<?php namespace App\Models;

use CodeIgniter\Model;
use App\Entities\Ingredient;

class IngredientModel extends Model
{
    protected $table = 'ingredient';
    protected $returnType = Ingredient::Class;

    protected $allowedFields = [
        'nom',
        'quantite',
    ];
}

Les entités Recette et Ingredient

CodeIgniter 4 introduit un nouveau concept qui n'était pas présent dans la version 3, les entités. On définit une entité par table et au lieu de retourner un objet générique, le modèle retournera une instance de cette classe. Tout comme la classe Model, la classe Entity offre des fonctionnalités intéressantes, mais pour cet exemple on va seulement ajouter une variable qui contiendra la liste d'ingrédients de chaque recette. On n'a pas besoin d'ajouter de variables pour le id, le titre, etc. CodeIgniter va automatiquement y ajouter les champs de la table MySQL.

app/Entities/Recette.php

Voici l'entité Recette. CodeIgniter va y ajouter automatiquement les variables id, titre et instructions de la table recette. On ajoute un tableau $ingredients pour la liste d'ingrédients de cette recette :

<?php namespace App\Entities;

use CodeIgniter\Entity;

class Recette extends Entity
{
    public $ingredients;

    public function __construct (array $data = null)
    {
        parent::__construct($data);

        // Initialiser la liste d'ingrédients avec un tableau vide.
        $this->ingredients = [];
    }
}

app/Entities/Ingredient.php

Voici l'entité Ingrdient dans son expression la plus simple. CodeIgniter va y ajouter automatiquement les variables id, id_recette, nom et quantite de la table ingredient. On n'a donc rien d'autre à faire. La classe est vide et ne fait qu'étendre la classe Entity :

<?php namespace App\Entities;

use CodeIgniter\Entity;

class Ingredient extends Entity
{
}

La librairie MesRecettes

Pour simplifier l'obtention des recettes et leurs ingrédients, j'ai créé une librairie MesRecettes qui s'occupe de charger les modèles et obtenir les données.

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;
    }
}

Le controlleur RecettesController

Dans le controlleur, on charge la lirairie MesRecettes et on obtiens les données de la base de données MySQL. La fonction _donnees_bidon() définit dans la première partie peut être supprimée.

app/Controllers/RecettesController.php

<?php namespace App\Controllers;

use App\Libraries\MesRecettes;

class RecettesController extends BaseController
{
    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->getToutesLesRecettes(),
        ];

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

La vue liste_recettes

Dans la vue liste_recettes, on fait quelques ajustements pour afficher les données obtenues de la base de données. La variable recette n'est plus un simple array. C'est maintenant une instance de la classe App\Entities\Recette. Tout comme la variable ingredient n'est plus une string. C'est maintenant une instance de la classe App\Entities\Ingredient.

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 App\Entities\Ingredient $ingredient  Un ingrédient (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">

<?php foreach ($recettes as $recette): ?>
        <article>
            <h3><?= esc($recette->titre) ?></h3>
            <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>
        <hr>
<?php endforeach; ?>

    </div>

</main>

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

</body>
</html>

Deuxième partie terminée !

Vous pouvez maintenant rafaîchir la page http://ci4.test:8888 qui affichera toujours les trois recettes. À première vue il n'y a pas beaucoup de différences, mais les données sont maintenant obtenue de MySQL. Vous pouvez insérer des nouvelles recettes dans les tables recette et ingredient et rafraîchir la page pour voir les nouvelles recettes. Si on a beaucoup de recette, ça ne fait pas beaucoup de sens d'avoir une liste interminable. La liste de recettes devrait afficher seulement le titre et les détails de chaque recette devraient être affichés sur une nouvelle page. C'est ce que nous allons faire dès maintenant dans la troisième partie.