IncludeBeer

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

Publié le 18 avril 2021 Dernière mise à jour le 18 avril 2021
Image de l'article
Origine de l'image: Bram Naus

Dans la cinquième partie nous avons ajouté un formulaire de recherche. Maintenant, nous allons ajouter un formulaire qui va nous permettre de créer, modifier et supprimer des recettes (insert, update et delete). Nous allons aussi voir comment faire la validation des données reçues d'un formulaire.

Les nouvelles routes

D'abord, on doit planifier quelles seront les routes pour nos nouvelles fonctionnalités.

app/Config/Routes.php

$routes->get('/creer', 'RecettesController::creer');
$routes->get('/modifier/(:num)', 'RecettesController::modifier/$1');
$routes->get('/supprimer/(:num)', 'RecettesController::supprimer/$1');
$routes->post('/sauvegarder', 'RecettesController::sauvegarder');
$routes->post('/sauvegarder/(:num)', 'RecettesController::sauvegarder/$1');

La route /creer va afficher un formulaire vide, qui nous permettera de créer une nouvelle recette. La route /modifier/(:num) va afficher un formulaire avec les informations d'une recette pour nous permettre de la modifier. La route /supprimer/(:num) va supprimer une recette. Finalement la route /sauvegarder va sauvergarder une nouvelle recette et la route /sauvegarder/(:num) va sauvegarder une recette existante. Le paramètre (:num) correspond au id d'une recette et signifie qu'uniquement une valeur numérique est acceptée. Les deux routes qui font la sauvegarde sont définies pour la méthode post car c'est avec cette méthode que le formulaire d'édition est soumis :

<form action="http://ci4.test:8888/sauvegarder" method="post" accept-charset="utf-8">

Le deuxième paramètre des fonctions get() et post() est le nom de la classe du contrôleur et le nom de la fonction qui sera appelée. Par exemple, pour la route :

$routes->get('/modifier/(:num)', 'RecettesController::modifier/$1')

...on appelle la fonction modifier(int $id) de la classe RecettesController. Le premier segment de la route, (:num) dans notre exemple, est passé en paramètre à la fonction du contrôleur avec la syntaxe $1.

Nouveau fichier de configuration

Pour simplifier le formulaire d'édition, nous allons définir un nombre maximal d'ingrédients pouvant être ajoutés à une recette. Évidemment, on ne va pas coder cette valeur en dur dans le code car nous sommes des professionels, alors on va le faire selon les règles de l'art ! On pourrait être un peu lâche et simplement créer une constante dans le fichier app/Config/Constants.php. Mais la bonne pratique est de créer un fichier de configuration spécifique pour notre application, qui sera chargé seulement au besoin. Un fichier de configuration est simplement une classe qui étend la classe BaseConfig, dont toutes les propriétés publiques sont des items de configuration. Créez le fichier app/Config/Recette.php avec la propriété $nb_ingredient.

app/Config/Recette.php

<?php namespace Config;

use CodeIgniter\Config\BaseConfig;

class Recette extends BaseConfig
{
    // Nombre d'ingrédients par recette
    public $nb_ingredient = 6;
}

Le contrôleur RecettesController

Dans le contrôleur, nous allons ajouter les quatre nouvelles fonctions qu'on a ajouté dans la configuration des routes, soit creer(), modifier (int $id), supprimer (int $id) et sauvegarder (int $id = null). Ces fonctions vont charger notre nouveau fichier de configuration avec cette commande :

$config = config('Recette');

Ensuite, il suffit simplement de spécifier l'élément désiré : $config->nb_ingredient.

Ajoutez les fonctions suivantes au contrôleur RecettesController :

app/Controllers/RecettesController.php

public function creer()
{
    // Charger les fonctions utilitaires pour les formulaires
    helper('form');

    // Charger la configuration de notre application
    $config = config('Recette');

    $data = [
        'titre_page' => "Nouvelle recette",
        'max_ingredient' => $config->nb_ingredient,
    ];

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

public function modifier (int $id)
{
    // Créer une instance de notre librairie
    $mesRecettes = new MesRecettes();

    // Charger les fonctions utilitaires pour les formulaires
    helper('form');

    // Charger la configuration de notre application
    $config = config('Recette');

    $data = [
        'titre_page' => "Modifier une recette",
        'max_ingredient' => $config->nb_ingredient,
    ];

    /* 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('form_recette', $data);
}

public function supprimer (int $id)
{
    // À compléter...
    log_message('debug', "Supprimer recette id $id");
}

public function sauvegarder (int $id = null)
{
    // À compléter...
    log_message('debug', ($id === null) ? "Sauvegarder nouvelle recette" : "Sauvegarder recette id $id");
}

La vue liste_recettes : Nouveau bouton Créer

Sur la page de la liste de recettes, on ajoute un bouton pour créer une nouvelle recette. Ce bouton nous redirige vers la route /creer qui va afficher un formulaire vide :

anchor('/creer',
       'Nouvelle recette',
       ['class' => 'btn btn-outline-success my-1 mr-3'])

Voici la vue complète avec le nouveau bouton pour créer une recette :

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 $rech                              Critères de recherche
 * @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">

<?php if (session('erreurs') !== null) : ?>
        <div class="alert alert-danger">
            <?= implode('<br>', session('erreurs')) ?>
        </div>
<?php endif; ?>

<?php if (session('message') !== null) : ?>
        <div class="alert alert-success text-center">
            <?= session('message'); ?>
        </div>
<?php endif; ?>

        <h3>Liste des recettes</h3>
        <div class="my-3">
            <?= form_open('/', ['class' => 'form-inline']) ?>
                <?= form_input('rech_texte',
                               $rech['texte'] ?? '',
                               ['class' => 'form-control my-1 mr-3', 'placeholder' => "Texte"]) ?>

                <?= form_label("Nombre par page", 'rech_nb_par_page', ['class' => 'my-1 mr-2']) ?>

                <?= form_input('rech_nb_par_page',
                               $rech['nb_par_page'] ?? '',
                               ['id' => 'rech_nb_par_page', 'class' => 'form-control my-1 mr-3', 'style' => 'width:70px']) ?>

                <?= form_submit('rech_submit',
                                "Recherche",
                                ['class' => 'btn btn-outline-primary my-1 mr-3']) ?>

                <?= anchor('/creer',
                           'Nouvelle recette',
                           ['class' => 'btn btn-outline-success my-1 mr-3']) ?>

            <?= form_close() ?>
        </div>

        <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; 2021 <?= anchor('/', "Mon site de recettes")?></p>
</footer>

</body>
</html>

La vue recette : Nouveaux boutons Modifier et Supprimer

Sur la page d'une recette, on ajoute deux boutons pour modifier ou supprimer cette recette. Le bouton pour modifier nous redirige vers la route /modifier qui va afficher un formulaire avec les données de la recette à modifier :

<?= anchor("/modifier/{$recette->id}",
           'Modifier',
           ['class' => 'btn btn-outline-primary']) ?>

Le bouton pour supprimer nous demande une confirmattion avec une alerte JavaScript. Si on confirme, on est redirigé vers la route /supprimer qui va faire la suppression de la recette puis nous rediriger vers la liste :

<?= anchor("/supprimer/{$recette->id}",
           'Supprimer',
           [
             'class' => 'btn btn-outline-danger',
             'onClick' => "return confirm('Voulez-vous vraiment supprimer cette recette ?');"
           ]) ?>

Voici la vue complète avec les nouveaux boutons pour modifier ou supprimer une recette :

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><?= nl2br( esc($recette->instructions) ) ?></p>
        </article>

        <div>
            <?= anchor("/modifier/{$recette->id}",
                       'Modifier',
                       ['class' => 'btn btn-outline-primary']) ?>

            <?= anchor("/supprimer/{$recette->id}",
                       'Supprimer',
                       [
                         'class' => 'btn btn-outline-danger',
                         'onClick' => "return confirm('Voulez-vous vraiment supprimer cette recette ?');"
                       ]) ?>
        </div>

    </div>

</main>

<footer>
    <p class="text-center">&copy; 2021 <?= anchor('/', "Mon site de recettes")?></p>
</footer>

</body>
</html>

Nouvelle vue form_recette : Formulaire d'édition

Le formulaire d'édition soumet les données vers la route /sauvegarder. On ajoute le id de la recette si c'est une recette existante :

form_open('/sauvegarder' . (isset($recette) ? "/{$recette->id}" : ""))

La fonction old() retourne l'ancienne valeur d'un champ si jamais la validation ne passe pas et qu'on retourne dans le formulaire : old('titre', $recette->titre ?? '', false). On utilise l'opérateur ?? pour déterminer si le champ existe ou non. Ça évite d'avoir à appeler la fonction isset(). Le paramètre false indique de ne pas appeller la fonction esc() qui "escape" les caractères HTML. On ne veut pas l'appeller, car elle est déjà appellée par la fonction form_input() et on ne veut pas se retrouver avec des caractères "escapé" deux fois.

Vous remarquerez l'utilisation d'une variable $max_ingredient. Elle sera définit dans le contrôleur et sa valeur sera obtenue à partir du fichier de configuration.

Créez une nouvelle vue pour le formulaire d'édition :

app/Views/form_recette.php

<?php
/**
 * @var string $titre_page                   Le titre de la page (créée automatiquement par CI via le tableau $data)
 * @var int $max_ingredient                  Nombre maximum d'ingrédients pour une recette
 * @var App\Entities\Recette $recette        La recette
 */
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<title><?= esc($recette->titre ?? "Nouvelle recette") ?></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: 1.5rem;
}
</style>

</head>

<body>

<main role="main" class="container">

    <div class="titre">
        <h1>
            <?= (isset($recette) ? "Modifier une recette" : "Nouvelle recette") ?>
        </h1>
    </div>

<?php if (session('erreurs') !== null) : ?>
    <div class="alert alert-danger">
        <?= implode('<br>', session('erreurs')) ?>
    </div>
<?php endif; ?>

<?php if (session('message') !== null) : ?>
    <div class="alert alert-success text-center">
        <?= session('message'); ?>
    </div>
<?php endif; ?>

    <div class="container">

        <div class="mb-3">
            <?= form_open('/sauvegarder' . (isset($recette) ? "/{$recette->id}" : "")) ?>

                <?= form_label("Titre",
                               'form_titre',
                               ['class' => 'form-label']) ?>

                <?= form_input('titre',
                               old('titre', $recette->titre ?? '', false),
                               ['class' => 'form-control mb-3']) ?>

                <?= form_label("Ingrédients", '', ['class' => 'form-label']) ?>

                <table class="table table-borderless">
                    <thead>
                        <tr>
                            <th scope="col">#</th>
                            <th scope="col">Quantité</th>
                            <th scope="col">Ingrédient</th>
                        </tr>
                    </thead>
                    <tbody>
<?php for ($i = 0; $i < $max_ingredient; $i++): ?>
                        <tr>
                            <th scope="row"><?= ($i + 1) ?></th>
                            <td>
                                <?= form_input("quantite_ingredient_{$i}",
                                               old("quantite_ingredient_{$i}", $recette->ingredients[$i]->quantite ?? '', false),
                                               ['class' => 'form-control']) ?>
                            </td>
                            <td>
                                <?= form_input("nom_ingredient_{$i}",
                                               old("nom_ingredient_{$i}", $recette->ingredients[$i]->nom ?? '', false),
                                               ['class' => 'form-control']) ?>
                            </td>
                        </tr>
<?php endfor; ?>
                    </tbody>
                </table>

                <?= form_label("Préparation",
                               'form_instruction',
                               ['class' => 'form-label']) ?>

                <?= form_textarea('instructions',
                                  old('instructions', $recette->instructions ?? '', false),
                                  ['id' => 'form_instruction', 'class' => 'form-control mb-3']) ?>

                <?= form_submit('form_submit',
                                "Sauvegarder",
                                ['class' => 'btn btn-outline-primary my-1']) ?>
            <?= form_close() ?>
        </div>

    </div>

</main>

<footer>
    <p class="text-center">&copy; 2021 <?= anchor('/', "Mon site de recettes")?></p>
</footer>

</body>
</html>

Le contrôleur RecettesController

À ce point-ci il est possible d'afficher le formulaire d'édition quand on clique sur Modifier. Mais rien ne se passe quand on clique Sauvegarder ou Supprimer. On va donc ajouter les fonctions supprimer() et sauvegarder() à notre contrôleur. Ces fonctions vont faire usage des nouvelles fonctions supprimerRecette() et sauvegarderRecette() qui seront ajoutées à notre librairie. Ces fonctions retournent true ou false en cas de succès ou d'erreur. Si l'opération se termine avec succès, on fait une redirection vers la liste de recettes avec un message qui confirme ce qui vient d'être fait : return redirect()->to('/')->with('message', "..."). Si l'opération a échouée, on fait une redirection vers la liste de recettes avec un message d'erreur : return redirect()->to('/')->with('erreurs', $mesRecettes->getErreurs()). Cette commande définit une variable message ou erreurs dans la session de l'utilisateur avec le texte du message. Il est ensuite affiché dans la vue avec <?= session('message') ?> ou <?= implode('<br>', session('erreurs')) ?>.

if ($mesRecettes->supprimerRecette($id))
{
    return redirect()->to('/')->with('message', "La recette a été supprimée avec succès.");
}
else
{
    return redirect()->to('/')->with('erreurs', $mesRecettes->getErreurs());
}

Validation

Dans le cas de la sauvegarde, la validation est plus complexe. On doit valider la valeur de chacun des champs du formulaire. Le contrôleur nous facilite la vie grâce à sa fonction validate(). Il suffit de lui passer les règles pour chacun des champs de notre formulaire et elle se charge d'obtenir les valeurs et de les passer à la classe Validation. En cas d'erreur, on retourne sur la page du formulaire : redirect()->back(), en lui passant les données du formulaire : withInput(), pour que l'utilisateur puisse faire les corrections requises. On obtient aussi les messages d'erreurs générés par la classe de validation qui va nous indiquer le nom des champs problématiques et la raison de l'échec : $this->validator->getErrors().

if ( ! $this->validate($regles))
{
    return redirect()->back()->withInput()->with('erreurs', $this->validator->getErrors());
}

Remplacez les fonctions suivantes dans le contrôleur RecettesController :

app/Controllers/RecettesController.php

public function supprimer (int $id)
{
    log_message('debug', "Supprimer recette id $id");

    // Créer une instance de notre librairie
    $mesRecettes = new MesRecettes();

    if ($mesRecettes->supprimerRecette($id))
    {
        return redirect()->to('/')->with('message', "La recette a été supprimée avec succès.");
    }
    else
    {
        return redirect()->to('/')->with('erreurs', $mesRecettes->getErreurs());
    }
}

public function sauvegarder (int $id = null)
{
    log_message('debug', ($id === null) ? "Sauvegarder nouvelle recette" : "Sauvegarder recette id $id");

    // Charger la configuration de notre application
    $config = config('Recette');

    /*
     * Définir les règles de validation du formulaire
     */
    $regles = [
        'titre' => [
            'label' => "Titre",
            'rules' => "required|max_length[100]|is_unique[recette.titre,id,{$id}]"
        ],
        'instructions' => [
            'label' => "Instructions",
            'rules' => "required|string"
        ],
    ];

    for ($i = 0; $i < $config->nb_ingredient; $i++)
    {
        $no_ingredient = $i + 1;

        $regles["quantite_ingredient_{$i}"] = [
            'label' => "Quantité de l'ingrédient {$no_ingredient}",
            'rules' => "permit_empty|string|max_length[10]|required_with[nom_ingredient_{$i}]"
        ];

        $regles["nom_ingredient_{$i}"] = [
            'label' => "Nom de l'ingrédient {$no_ingredient}",
            'rules' => "permit_empty|string|max_length[50]|required_with[quantite_ingredient_{$i}]"
        ];
    }

    /*
     * Valider les données du formulaire
     */
    if ( ! $this->validate($regles))
    {
        return redirect()->back()->withInput()->with('erreurs', $this->validator->getErrors());
    }

    // Créer une instance de notre librairie
    $mesRecettes = new MesRecettes();

    // Obtenir les données du formulaire
    $form_data_recette = [
        'titre' => $this->request->getPost('titre'),
        'instructions' => $this->request->getPost('instructions'),
    ];

    // Extraire et valider les ingrédients de cette recette
    $form_data_ingredients = [];

    for ($i = 0; $i < $config->nb_ingredient; $i++)
    {
        if ( ! empty($this->request->getPost("quantite_ingredient_{$i}")) &&
             ! empty($this->request->getPost("nom_ingredient_{$i}")))
        {
            $form_data_ingredients[] = [
                'quantite' => $this->request->getPost("quantite_ingredient_{$i}"),
                'nom' => $this->request->getPost("nom_ingredient_{$i}"),
            ];
        }
    }

    // Obtenir les données du formulaire et les sauvegarder
    if ($mesRecettes->sauvegarderRecette($id, $form_data_recette, $form_data_ingredients))
    {
        return redirect()->to('/')->with('message', "Recette sauvegardée avec succès");
    }
    else
    {
        return redirect()->back()->withInput()->with('erreurs', $mesRecettes->getErreurs());
    }
}

La librairie MesRecettes

Maintenant il ne reste qu'à ajouter les nouvelles fonctions pour supprimer et sauvegarder une recette dans notre librairie MesRecettes. Ces nouvelles fonctions sont appelées par le contrôleur.

La fonction supprimerRecette(int $id) supprime tous les ingrédients dont le id_recette correspond à la recette à supprimer, puis on supprime la recette elle-même. Pour ce faire, nous appellon la fonction $this->recetteModel->delete($id) qui s'occupe de faire le DELETE dans la base de données. La fonction $this->recetteModel->db->affectedRows() retourne le nombre de rangées supprimées, ce qui nous permet de savoir si la suppression a bien été faite.

Ensuite, la fonction sauvegarderRecette(?int $id, array $form_data_recette, array $form_data_ingredients) nous permet de sauvegarder une recette et ses ingrédients dans la base de données. C'est la même fonction qui s'occupe de céer une nouvelle recette ou de mettre à jour une recette existante. Le premier paramètre est le id de la recette. Son type est ?int, ce qui signifie qu'on accepte une valeur numérique ou null. Dans le cas d'une nouvelle recette, elle n'a pas encore d'id alors on passe null. Si on a un id, on obtiens la recette avec la fonction $this->recetteModel->find($id) et on écrase les valeurs de l'objet $recette par celles entrées dans le formulaire avec la fonction $recette->fill($form_data_recette). Pour éviter un update inutile, la fonction $recette->hasChanged() nous permet de savoir si quelque chose à changé. Finalement, la fonction $this->recetteModel->save($recette) s'occupe de faire soit un INSERT ou un UPDATE selon le cas. Si on crée une nouvelle recette, $this->recetteModel->db->insertID() nous retourne le id de la recette qui vient d'être créée.

Voici le code des nouvelles fonctionnalités ajoutées à la librairie MesRecettes. Le contenu de certaines fonctions a été retiré pour éviter que le code soit trop long inutilement. Référez-vous à la cinquième partie pour le code de ces fonctions.

app/Libraries/MesRecettes.php

<?php namespace App\Libraries;

use App\Models\RecetteModel;
use App\Models\IngredientModel;
use App\Entities\Recette;
use App\Entities\Ingredient;

class MesRecettes
{
    public $recetteModel;
    public $ingredientModel;
    private $erreurs;

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

    /**
     * Définir les messages d'erreurs
     * @param array|string $erreurs
     */
    private function setErreurs ($erreurs)
    {
        // Si on reçoit autre chose qu'un array, convertir en array
        $this->erreurs = is_array($erreurs) ? $erreurs : (array)$erreurs;
    }

    /**
     * Retourner les messages d'erreurs
     * @return array
     */
    public function getErreurs(): array
    {
        return $this->erreurs;
    }

    public function getListeRecettes (array $rech)
    {
        // Voir la partie 5 de cette série d'articles
    }

    public function getRecetteParId (int $id)
    {
        // Voir la partie 5 de cette série d'articles
    }

    public function getRecetteParSlug (string $slug)
    {
        // Voir la partie 5 de cette série d'articles
    }

    /**
     * Supprimer une recette et ses ingrédients
     * @param int $id
     * @return bool
     */
    public function supprimerRecette (int $id): bool
    {
        // D'abord supprimer les ingrédients de cette recette
        if ( ! $this->ingredientModel
                ->where( ['id_recette' => $id] )
                ->delete() )
        {
            $this->setErreurs($this->ingredientModel->errors());
            return false;
        }

        // Supprimer la recette
        if ( ! $this->recetteModel->delete($id))
        {
            $this->setErreurs($this->recetteModel->errors());
            return false;
        }

        // Obtenir le nombre de rangées supprimées
        $nb_delete = $this->recetteModel->db->affectedRows();
        log_message('debug', "$nb_delete recette supprimée");

        // Si aucune rangées n'a été supprimées
        if ($nb_delete === 0)
        {
            $this->setErreurs("Aucune recette trouvée avec le id $id");
            return false;
        }

        return true;
    }

    /**
     * Sauvegarder une recette et ses ingrédients
     * @param int $id
     * @param array $form_data_recette
     * @param array $form_data_ingredients
     * @return bool
     */
    public function sauvegarderRecette (?int $id, array $form_data_recette, array $form_data_ingredients): bool
    {
        // Si on a un id, obtenir la recette à modifier
        if ( ! is_null($id))
        {
            if ( ! $recette = $this->recetteModel->find($id) )
            {
                $this->setErreurs("Aucune recette trouvée");
                return false;
            }
        }
        else
        {
            $recette = new Recette();
        }

        // Remplir les champs de l'objet Recette avec les données du formulaire
        $recette->fill($form_data_recette);
        log_message('debug', "Recette: " . print_r($recette, true));

        if ($recette->hasChanged())
        {
            // La fonction save() s'occupe de faire un INSERT ou un UPDATE, selon le cas.
            if ( ! $this->recetteModel->save($recette))
            {
                $this->setErreurs($this->recetteModel->errors());
                return false;
            }
        }

        // Si c'est une nouvelle recette, obtenir son ID
        if (is_null($id))
        {
            $id = $this->recetteModel->db->insertID();
            log_message('debug', "Nouvelle recette id $id");
        }
        // Si ce n'est pas une nouvelle recette, supprimer ses ingrédients
        else
        {
            if ( ! $this->ingredientModel
                    ->where( ['id_recette' => $id] )
                    ->delete() )
            {
                $this->setErreurs($this->ingredientModel->errors());
                return false;
            }
        }

        // Ajouter les nouveaux ingrédients de cette recette
        foreach ($form_data_ingredients as $data_ingredient)
        {
            // Associer l'ingrédient à la recette
            $data_ingredient['id_recette'] = $id;

            log_message('debug', "Data Ingrédient: " . print_r($data_ingredient, true));

            // Créer une entité Ingredient avec les données du formulaire
            $ingredient = new Ingredient();
            $ingredient->fill($data_ingredient);

            log_message('debug', "Ingrédient: " . print_r($ingredient, true));

            // Insérer cet ingrédient dans la base de donnée
            if ( ! $this->ingredientModel->save($ingredient) )
            {
                $this->setErreurs($this->ingredientModel->errors());
                return false;
            }
        }

        return true;
    }
}

Vous avez peut-être remarqué la présence de deux nouvelles fonctions dans notre librairie : setErreurs($erreurs) et getErreurs(). Elle permettent respectivement de garder en mémoire ou de retourner les erreurs survenues pendant le traitement. On aurait pus gérer les erreurs avec des Exception et des try/catch, mais dans ce cas-ci je pense que cette approche est plus simple. En cas d'erreur, le contrôleur peut obtenir les messages d'erreurs et les passer à la vue pour les présenter à l'utilisateur.

Pour vous aider à comprendre qui fait quoi dans le framework CodeIgniter, voici d'où viennent les fonctions utilisées dans notre librairie. Les fonctions find() et save() sont définit dans la classe CodeIgniter\BaseModel qui se trouve à être la classe de base de la classe CodeIgniter\Model. Les fontions fill() et hasChanged() sont définit dans la classe CodeIgniter\Entity qui est la classe de base des objets que nous manipulons avec la base de données. Généralement une entité représente une rangée d'une table MySQL. Les fonctions affectedRows() et insertID() sont définit dans la classe abstraite CodeIgniter\Database\BaseConnection et sont implémentées dans la classe CodeIgniter\Database\MySQLi\Connection.

Modèle et entité

Pour que CodeIgniter nous permette d'ajouter des ingrédients, il faut ajouter le champ id_recette dans la liste des champs modifiables. Ajoutez le champ id_recette au modèle IngredientModel :

app/Models/IngredientModel.php

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

Une fonctionnalité interessante de la classe Entity, est que s'il existe une fonction portant le même nom qu'une de ses propriétés et est préfixée du mot set, elle est appellée automatiquement lorsqu'on assigne une valeur à cette propriété. Par exemple, pour la propriété titre on peux créer une fonction setTitre() dans l'entité Recette et lorsqu'on va assigner une valeur au champ titre, la fonction va être automatiquement appellée. C'est ici qu'on va généré le slug de la recette avec la fonction mb_url_title().

Ajoutez la fonction setTitre() à l'entité Recette :

app/Entities/Recette.php

// Cette fonction est automatiquement appellée quand on définie la valeur de $recette->titre
public function setTitre (string $titre)
{
    $this->attributes['titre'] = $titre;

    // Définir automatiquement le "slug" à partir du titre de la recette.
    $this->attributes['slug'] = mb_url_title($titre, '-', TRUE);
}

C'était la dernière partie de cette série !

Vous pouvez maintenant rafraîchir la page http://ci4.test:8888 qui affichera maintenant un bouton pour créer une nouvelle recette. Sur la page d'une recette, deux nouveaux boutons sont présents pour modifier ou supprimer la recette. Cet article conclu cette série d'articles. Il y a évidemment plein d'autres améliorations qui pourrait y être ajoutées, mais ça couvre les fonctionnalités de bases d'une application web créée avec CodeIgniter 4.

Beer Si vous appréciez mes tutoriels, vous pouvez me payer l'équivalent d'un café, une bière ou un lunch !

Paypal