Aller au contenu

Blog

Vous êtes maintenant connecté à votre base de données. Il nous reste à interagir avec les tables.

Découvrons les concepts en créant un mini-blog.

Nous allons utiliser la structure suivante :

/blog/
├── index.php        # Page principale
│── /public/         # Fichiers accessibles au navigateur
│   ├── css/         # Styles CSS
│   ├── img/         # Images du site

│── /inc/            # Fichiers réutilisables
│   |── .htaccess    # Configuration Apache    

│── /php/            # Fichiers php
│   |── .htaccess    # Configuration Apache    

Vous pouvez télécharger la structure et les fichiers de départ GitHub >>

Astuce

Pour télécharger les sources,

- cliquer sur le bouton vert `<> Code`
- cliquer sur `Download Zip`

Download souce

Base de données

Créez la table et les insertions à l'aide du fichier .sql. Pour rappel >>

Fichier de configuration

Complétez le fichier des configurations à l'aide de vos informations.

Lister les articles

Requêtes sans paramètres

Analysez la page d'accueil. Le contenu des articles doit provenir de la base de données. Quelle est la requête sql utilisée pour obtenir tous les articles, triés par ordre d'encodage ?

Soluce

SELECT * FROM blog_article ORDER BY id;

Cette requête ne contient aucun paramètre donné.

Classe dédiée aux articles

Nous allons créer une classe pour définir le concept d'article de blog. Cette classe va donc représenter un article d'un blog et ses attributs vont correspondre aux colonnes de la table blog_article de la base de données. Cette classe ne va pas possèder de méthodes car elle sera utilisée comme un simple modèle de données.

Question

Les attributs doivent-ils être publics ou privés ? Quelles sont les conséquences du choix réalisé ?

Une deuxième classe va être créée et sera responsable de gérer la persistance des données.

Par exemple, cette classe va contenir une méthode qui aura comme objectif d'aller rechercher tous les articles présents dans la table blog_article. Prenons le temps de réfléchir à cette méthode. Quelles seront les étapes nécessaires ?

Soluce
  • se connecter à la base de données via la classe DBLink Rappel >>
  • exécuter la requête
  • retourner le résultat
  • gérer les erreurs éventuelles
  • se déconnecter de la base de données

Exemple de classes

<?php

namespace Blog;

require 'db_link.inc.php';

use DB\DBLink;
use PDO;

/**
* Représente un article du blog
*/
class Article
{
    public $id;
    public $titre;
    public $contenu;
}

/**
* Classe ArticleRepository : gestionnaire des articles
*/

class ArticleRepository
{
    const TABLE_NAME = 'blog_article';
    /**
    * Récupère tous les articles depuis la base de données.
    *
    * @param string &$message Référence à une variable de message d'état
    * @return array Tableau d'objets Article | Tableau vide
    */
    public function getAllArticles(&$message)
    {
        $result = array();
        $bdd = null;
        try {
            $bdd = DBLink::connect2db(MYDB, $message);
            if (!$bdd) return $result;
            $result = $bdd->query("SELECT * FROM " . self::TABLE_NAME . " order by id", PDO::FETCH_CLASS, "Blog\Article");
        } catch (\Exception $e) {
            $message .= $e->getMessage() . '<br>';
        } finally {
            DBLink::disconnect($bdd);
        }
        return $result;
    }

Explication du code

<?php 
namespace Blog;

require 'db_link.inc.php'; 
use DB\DBLink;
use PDO;
namespace Blog; : Définit l'espace de noms Blog, ce qui permet d'éviter les conflits avec d'autres classes ayant le même nom dans le projet.

require 'db_link.inc.php'; : Importe un fichier externe (db_link.inc.php), qui contient la classe DBLink qui gére la connexion à la base de données.

use DB\DBLink; et use PDO;

- `DBLink` est une classe dans le namespace DB, utilisée pour la connexion à la base de données.

- `PDO` est la classe native de PHP permettant d'interagir avec une base de données via SQL.
<?php 
class Article
{
    public $id;
    public $titre;
    public $contenu;
}
  • Cette classe représente un article de blog.
  • Ses propriétés (id, titre, contenu) correspondent aux colonnes de la table blog_article dans la base de données. Si vous travaillez avec des attributs privés, il vous faut utiliser des getter et des setter (/!\methodes magiques)
  • Il n'y a pas de méthode dans cette classe, elle est utilisée comme un simple modèle de données.
<?php 
class ArticleRepository
{

}

Cette classe va contenir les différentes méthodes pour interagir avec la base de données concernant les articles.

<?php 
public function getAllArticles(&$message)
    {
        $result = array();
        $bdd = null;
        try {
            $bdd = DBLink::connect2db(MYDB, $message);
            if (!$bdd) return $result;
            $result = $bdd->query("SELECT * FROM " . self::TABLE_NAME . " order by id", PDO::FETCH_CLASS, "Blog\Article");
        } catch (\Exception $e) {
            $message .= $e->getMessage() . '<br>';
        } finally {
            DBLink::disconnect($bdd);
        }
        return $result;
    }

<?php 
$bdd = DBLink::connect2db(MYDB, $message);
if (!$bdd) return $result;
Cette méthode tente d'établir une connexion à la base de données. Si $bdd est null ou false, la connexion a échoué et la fonction retourne un tableau vide ($result). /! Dans ce cas, un message est assigné à la variable $message (voir connect2db()).

<?php 
$result = $bdd->query("SELECT * FROM " . self::TABLE_NAME . " order by id", PDO::FETCH_CLASS, "Blog\Article");

Méthode query() de PDO :

La méthode $bdd->query() exécute la requête SQL sur la base de données. Si la requête réussit, elle retourne un objet de type PDOStatement.

Mode de récupération PDO::FETCH_CLASS

PDO::FETCH_CLASS, indique à PDO de renvoyer chaque ligne du résultat sous la forme d'un objet d'une classe donnée.

Spécification de la classe

"Blog\Article", indique que chaque tuple doit être converti en une instance de la classe Blog\Article.

En résumé : Chaque tuple de la table sera converti en objet (instance de la classe Article du nameSpace Blog) PDO mappe les colonne de la table sur les propriétés de l'objet. Il est donc important que ceux-ci soient identiques aux noms de colonnes de la table.

Stockage du résultat

Le résultat de l'appel à query() (un PDOStatement contenant les objets Article) est affecté à la variable $result.

<?php 
catch (\Exception $e) {
    $message .= $e->getMessage() . '<br>';
}

Si une erreur survient, l'exception est capturée, et son message est stocké dans $message.

<?php 
finally {
    DBLink::disconnect($bdd);
}
Fermeture de la connexion à la base de données en appelant DBLink::disconnect($bdd).

<?php 
return $result;

Si un erreur est survenue, on retourne un tableau vide sinon, on retourne un objet de type PDOStatement (et qui comprend des instances de la classe Article), qui est itérable.

Select et PDO

L'exemple précédent vous a montré l'utilisation de PDO::FETCH_CLASS. Cependant, PDO offre plusieurs options pour récupérer les enregistrements d'un jeu de résultats, en spécifiant le mode de récupération (fetch) :

  • comme un tableau indicé (PDO::FETCH_NUM),
  • comme un tableau associatif (PDO::FETCH_ASSOC),
  • comme un tableau combiné associatif et indicé (PDO::FETCH_BOTH (défaut)),
  • comme un objet (PDO::FETCH_CLASS)
  • Pour la version "objet", l'appel au constructeur de la classe peut être forcé avant d'affecter les propriétés en spécifiant les styles PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE.

Référence officielle >>

Gestion des erreurs

La liaison d'un site web à une base de données peut provoquer des exceptions dues à une perte temporaire de connexion à la base de données ou à des données corrompues soumises par l'utilisateur. Pour des questions de sécurité et d'ergonomie, il faut toujours capturer ces exceptions. En effet, les messages bruts fournis par les exceptions sont perturbants pour le néophyte et donnent des indices au hacker sur le code source.

Tout appel à des fonctions de PDO doit être encapsulé par un bloc "try catch" pour capturer et gérer les exceptions.

⚠ Warning: Testez votre code en indiquant par exemple une erreur dans le nom de la BD ou dans le nom de la table. Observez les messages d'erreur affichés. Dans l'exemple précédent, beaucoup trop d'infos sont affichées (mais qui sont pratiques pour débuguer lorsqu'on débute ). Pour votre projet, à vous d'imaginer votre technique de gestion d'erreurs en fonction des informations que vous souhaitez renvoyer à l'utilisateur.

Afficher la liste des articles

La page index.php doit afficher la liste des articles.

  • Supprimez tous les articles excepté un.
  • En début de fichier, nous allons inclure le fichier contenant la classe Article.

Méthode :

  • Créez un objet ArticleRepository
  • Appelez la méthode getAllArticles()
  • Vérifiez si la méthode retourne bien des articles
  • Si un message d'erreur existe l'afficher,
  • Sinon, bouclez sur l'objet PDOStatement
  • Pour chaque article, affichez les propriétés désirées
Exemple de solution
<?php
include 'php/db_posts.inc.php';

use Blog\ArticleRepository;

$message = '';
$articleRepository = new ArticleRepository();
$articles = $articleRepository->getAllArticles($message);
$message .= ($articles && $articles->rowCount() === 0) ? 'Pas d\'article pour le moment' : '';

?> 
Exemple de solution
<?php
if (!empty($message)) {
    echo $message;
} else {
    foreach ($articles as $article) { ?>
        <article class="item">
            <p>#Article <?= nettoyage($article->id) ?></p>
            <h3><?= $article->titre ?></h3>
            <a href=''>&gt;&gt;Lire l'article</a>
        </article>
<?php  }
}
?>

⚠ Warning: Attention : ne jamais faire confiance aux données. Nous allons créer une fonction de nettoyage.

Fonctions utils

Utilisez cette fonction dans le code qui affiche la liste des articles.

Lien vers l'article complet

Lorsqu'on clique sur le lien >>Lire l'article, le détail de l'article doit s'afficher.
Imaginons qu'on désire le détail de l'article numéro 10, quelle sera la requête adéquate ?

Soluce

SELECT * FROM blog_article WHERE id = 10;

La page doit donc recevoir le numéro de l'article. Nous allons passer le numéro de l'article en utilisant un paramètre GET. Exemple : http://monSite/public/article.php?id=3

Dans la page index, modifiez la structure du lien pour passer le numéro de l'article.

Soluce
<a href='public/article.php?id=<?= nettoyage($article->id) ?>'>&gt;&gt;Lire l'article</a>
Astuce

Très souvent, les URLs sont ré-écrites et on ne voit pas apparaitre les paramètres get. Par exemple, l'url http://monSite/public/article.php?id=3 pourrait devenir http://monSite/article/3

Cette technique s'appelle URL Rewriting

Cela permet une meilleure lisibilité, un meilleur référencement et une meilleure sécurité.

Si vous êtes à l'aise avec les concepts vus, n'hésitez pas à tester cette technique.

Requêtes paramétrées

Les requêtes vers une base de données permettent de rechercher (SELECT), insérer (INSERT), mettre à jour (UPDATE) ou supprimer (DELETE) des données. Les requêtes sont écrites en respectant la syntaxe SQL du SGBD (ici, en l'occurrence, c'est MySQL).

Lorsque des paramètres sont utilisés, vous pouvez utiliser soit

  • Requêtes non préparées : elles sont construites en concaténant des chaînes de caractères avec des variables PHP.

  • Requêtes préparées : elles utilisent un template SQL dans lequel les valeurs sont liées après la préparation.

Les requêtes préparées sont plus sécurisées, mais un peu plus lentes car exécutées en deux étapes :

  • Préparation : vérification syntaxique et allocation des ressources par MySQL.

  • Exécution : liaison des paramètres et exécution effective.

Les requêtes non préparées présentent un risque d’injection SQL, surtout si elles intègrent directement des données utilisateur sans contrôle.

Requête non préparée

L'exécution d'une requête non préparée nécessite d'abord de construire la chaine de caractères de la requête. Lorsque cette requête doit inclure la valeur d'une variable PHP, surtout si celle-ci provient d'une donnée fournie par l'utilisateur (via formulaire, ...), il faut se protéger des attaques par injection de code SQL.

Exemple, la requête contient le code "SELECT * FROM articles WHERE reference = '".$_POST['ref']."';" et la variable $_POST['ref'] reçue de l'utilisateur vaut "0'; DELETE FROM users WHERE 1;" du coup la requête exécutée par le serveur devient SELECT * FROM articles WHERE reference = '0'; DELETE FROM users WHERE 1;'; !! Malgré l'erreur de syntaxe finale, la requête de suppression serait exécutée (pour autant que la table users existe bien entendu).

La fonction real_escape_string échappe certains caractères spéciaux (NUL, \n, \r, \, ', ") pour éviter les injections de code SQL. Utilisée sur $_POST['ref'], la requête serait: SELECT * FROM articles WHERE reference = '0\'; DELETE FROM users WHERE 1;'; , du coup la requête de suppression ne serait pas exécutée.

Requête préparée

Une requête préparée est une méthode sécurisée pour exécuter des requêtes SQL avec PHP et PDO (PHP Data Objects). Elle permet de séparer la structure de la requête des valeurs insérées, réduisant ainsi les risques d’injection SQL.

Pour une requête préparée, il faut d'abord définir le template de requête en utilisant le caractère :nomChoisi pour chaque paramètre (->prepare()). Ensuite, chaque paramètre de la requête doit être lié (->bindValue ()). Enfin, la requête peut être exécutée(->execute()).

Détail de l'article

Methode getArticleById()

Nous allons créer une nouvelle méthode dans la classe ArticleRepository Cette méthode doit recevoir en paramètre le numéro de l'article et la variable message. Comme précédemment, nous allons utiliser un blog try...catch

Quelles sont les étapes nécessaires ?

Soluce
  • la méthode doit recevoir l'id de l'article à afficher
  • se connecter à la base de données via la classe DBLink Rappel >>
  • préparer la requête
  • lier le paramètre (id de l'article)
  • executer la requête
  • retourner le résulat
  • gérer les erreurs éventuelles
  • se déconnecter de la base de données

Exemple de classe

 public function getArticleById($id, &$message)
{
    $result = array();
    $bdd    = null;
    try {
        $bdd  = DBLink::connect2db(MYDB, $message);
        if (!$bdd) return $result;
        $stmt = $bdd->prepare("SELECT * FROM " . self::TABLE_NAME . " WHERE id = :id_article");
        $stmt->bindValue(':id_article', $id);
        if ($stmt->execute()) {
            $obj = $stmt->fetchObject('Blog\Article');
            $result = ($obj !== false ? $obj : null);
        } else {
            $message .= 'Une erreur système est survenue.<br> 
                Veuillez essayer à nouveau plus tard ou contactez l\'administrateur du site. 
                (Code erreur: ' . $stmt->errorCode() . ')<br>';
        }
    } catch (\Exception $e) {
        $message .= $e->getMessage() . '<br>';
    }
    DBLink::disconnect($bdd);
    return $result;
}

Explication du code Seuls les nouvelles notions sont expliquées

<?php 
$stmt = $bdd->prepare("SELECT * FROM " . self::TABLE_NAME . " WHERE id = :id_article");
            $stmt->bindValue(':id_article', $id);

prepare() : Prépare une requête sécurisée avec un paramètre :id_post (requête préparée)(le nom est choisi arbitrairement). bindValue(':id_post', $id) : Associe la valeur $id au paramètre :id_post pour éviter les injections SQL.

<?php 
if ($stmt->execute()) {
    $obj = $stmt->fetchObject('Blog\Article');
    $result = ($obj !== false ? $obj : null);
}

execute(): Exécute la requête SQL.

fetchObject('Blog\Article'):

  • Retourne un objet de type Blog\Article contenant les données de l'article.

  • Si aucun article n'est trouvé, retourne false.

$result = ($obj !== false ? $obj : null);:

  • Assigne l'objet récupéré à $resultsi la requête a réussi.

  • Si aucun article n'est trouvé, $result prend la valeur null.

Affichage du détail

Rappelez-vous, la page est appellée via une url du type http://monSite/public/article.php?id=6

A votre avis, pour une question de sécurité, que doit-on absolument faire ?

Soluce
  • Vérifier que la requête possède un paramètre nommé id
  • Vérifier que la valeur du paramètre id est bien de type numérique

Pour réaliser cette vérification, nous pouvons utiliser les filtres prévus par PHP : Rappel >>

Exemple
<?php
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id !== false && $id !== null) {
    //traitement
}else {
    $messageErreur = "Erreur : L'identifiant de l'article est invalide.";
} 

filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) :

  • Récupère et valide la valeur de id directement depuis la superglobale GET
  • Retourne un entier si la valeur est valide ou false en cas d'invalidité
  • Retourne null si la clé n'existe pas

Dans cet exemple, nous décidons d'afficher un message si une erreur est détectée; nous aurions pu choisir de rediriger la page (header location Référence officielle >>)

Que doit-on faire si l'id est valide ?

Soluce
  • Instanciez le classe ArticleRepository()
  • Appelez la méthode getArticleById en lui passant les paramètres attendus
  • Vérifiez si l'article existe
  • Affichez l'article ou le message d'erreur

Exemple de solution

<?php 
$articleRepository = new ArticleRepository();
$article = $articleRepository->getArticleById($id, $messageErreur);
if (!$article) {
    /*L'opérateur ternaire vérifie si $messageErreur est vide.
    Si c'est le cas, il attribue à $messageErreur le message d'erreur par défaut.
    Sinon, il conserve la valeur déjà présente dans $messageErreur. */
    $messageErreur = empty($messageErreur) ? "Erreur : L'article demandé n'existe pas." : $messageErreur;
}

Dans le body, il reste à afficher les valeurs contenues dans l'objet.

Exemple de solution

<main class="centrage boxOmbre">
<ul class="containerFlex">
    <li><i class="fa fa-arrow-left"></i> <a href="<?= BASE_URL ?>"> vers la liste des articles</a></li>
</ul>
<?php if (isset($article) && $article) { ?>
    <h1><?= nettoyage($article->titre) ?></h1>
    <p><?= nl2br(nettoyage($article->contenu)) ?></p>
<?php } else { ?>
    <div class="error-message">
        <p><?= nettoyage($messageErreur) ?></p>
    </div>
<?php } ?>
</main>

Création d'un article

Méthode insertArticle()

Quelle est la requête sql utilisée pour insérer un article ?

Soluce

INSERT INTO blog_article (titre, contenu) VALUES ("le titre", "le contenu");

Il s'agit d'une requête paramètrée, nous allons donc suivre la même méthodologie que pour la méthode getArticleById()

⚠ Warning: Les valeurs de l'article seront envoyées via une instance de la classe Article(). A votre avis, quel intérêt d'utiliser un objet ?

A vous de créer la méthode.

Exemple de solution
insertArticle()
<?php 
    /**
    * Insère un nouvel article dans la base de données.
    *
    * @param Article $article Objet représentant l'article à insérer (doit contenir `titre` et `contenu`).
    * @param string &$message Référence à une variable de message d'état
    * @return bool Retourne `true` si l'insertion est réussie, sinon `false`.
    *
    */
    public function insertArticle($article, &$message)
    {
        $noError = false;
        $bdd   = null;
        try {
            $bdd  = DBLink::connect2db(MYDB, $message);
            if (!$bdd) return $noError;
            $stmt = $bdd->prepare("INSERT INTO " . self::TABLE_NAME . " (titre, contenu) VALUES (:titre, :contenu)");
            $stmt->bindValue(':titre', $article->titre);
            $stmt->bindValue(':contenu', $article->contenu);
            if ($stmt->execute()) {
                $noError = true;
            } else {
                $message .= 'Une erreur système est survenue.<br> 
                    Veuillez essayer à nouveau plus tard ou contactez l\'administrateur du site. 
                    (Code erreur: ' . $stmt->errorCode() . ')<br>';
            }
            $stmt = null;
        } catch (\Exception $e) {
            $message .= $e->getMessage() . '<br>';
        }
        DBLink::disconnect($bdd);
        return $noError;
    }

Insertion et vérification

Quelles sont les différentes étapes à prévoir ?

Soluce
  • récuperer les valeurs des champs et les nettoyer, si elles existent
  • vérifier la soumission du formulaire
  • réaliser les vérifications
    • prévoir un tableau pour stocker les erreurs
    • vérifier les valeurs des champs (obligatoire à l'encodage -> non vide)
    • vérifier le nombre de caractères insérés pour le titre
  • effectuer le traitement uniquement si il n'y a pas d'erreur détectée

A vous de créer le code.

Exemple de solution
<?php 

$erreurs = [];
// récuperer les valeurs des champs et les nettoyer, si elles existent
$titre = nettoyage($_POST['titre'] ?? '');
$contenu = nettoyage($_POST['contenu'] ?? '');

//vérifier la soumission du formulaire
if (isset($_POST['btn_article'])) {

    //réaliser les vérifications
    if (empty($titre)) {
        $erreurs[] = 'Le titre ne peut pas être vide';
    } else if (mb_strlen($titre) > 100) {
        $erreurs[] = 'Le titre ne peut pas être excéder 100 caractères.';
    }     

    if (empty($contenu)) {
        $erreurs[] = 'Le contenu ne peut pas être vide';
    }

    //si pas d'erreurs, on réalise le traitement
    if (empty($erreurs)) {
        //traitement
    }
}

Quelle est le traitement à prévoir ?

Soluce

Rappelez-vous : la méthode insertArticle attend un objet comprenant le titre et le contenu.

  • Créez une instance de la classe Article()
  • Attribuez les valeurs aux attributs
  • Créez une instance de la classe ArticleRepository()
  • Appelez la méthode insertArticle
  • Si la méthode retourne vrai, on crée un message de confirmation
  • Sinon, on crée un message d'erreur

A vous de créer le code.

Exemple de solution
<?php 
    //Créez une instance de la classe `Article()`
    $post = new Article();

    // Attribuez les valeurs aux attributs
    $post->titre = $titre;
    $post->contenu = $contenu;

    // Créez une instance de la classe `ArticleRepository()`
    $articleRepository = new ArticleRepository();

    // Appelez la méthode `insertArticle`
    // Si la méthode retourne vrai, on crée un message de confirmation
    // Sinon, on crée un message d'erreur
    if ($articleRepository->insertArticle($post, $messageErreur)) {
        $message .= "Article correctement ajouté.";
        // réinitilisation à blanc des champs du formulaire
        $titre = $contenu = '';
    } else {
        $erreurs[]  = "Erreur technique. Veuillez contacter l'administrateur.";
        $erreurs[] = $messageErreur;
    }

Question

A votre avis, dans l'exmple de solution, pourquoi assigne-t-on la valeur de la variable$messageErreur au tableau des erreurs ?

N'oubliez pas la persistance des données dans le formulaire.

<label for id="titre">Titre *</label><input type="text" size="50" maxlength="50" id="titre" name="titre" value="<?= $titre ?>">
<label for id="contenu">Contenu *</label><textarea name="contenu" id="contenu"><?= $contenu ?></textarea>      

Prévoyez l'affichage des messages d'erreur ou de confirmation.

Dans notre blog, les messages d'état sont encadrés et de couleur définie en fonction de l'état.

blog-boxDanger blog-boxSuccess blog-boxSuccess

Question

Sur quel élément peut-on se baser pour afficher une box de confirmation ? Sur quel élément peut-on se baser pour afficher une box d'alerte ?

Observez la css. Quels sont les styles appliqués ?

Sur base de ces éléments, prévoyez le code (hors fonction générique).

Exemple de solution
<?php 
if (!empty($message)) { ?>
            <div class="box-alert color-success">
                <?= nettoyage($message); ?>
            </div>
        <?php
        }

        if (!empty($erreurs)) { ?>
            <div class="box-alert color-danger">
                <ul>
                    <?php
                    foreach ($erreurs as $erreur) {
                        echo '<li>' . $erreur . '</li>';
                    }
                    ?>
                </ul>
            </div>
<?php } ?>

Fonction générique

Lorsqu’une interaction se produit dans une interface, il est essentiel de prévoir des box d’état afin d’assurer un retour visuel clair pour l’utilisateur. Par exemple, lors de l'envoi d’un formulaire, l’utilisateur s’attend à recevoir un feedback indiquant si l’opération est en cours, réussie ou en échec. Sans cette indication, il peut ressentir de l’incertitude et ne pas savoir si son action a bien été prise en compte, ce qui peut nuire à son expérience.

Pourquoi prévoir des box d’état dès qu’il y a interaction ?

  • Visibilité et feedback utilisateur : Une interaction implique un changement d’état (succès, erreur, chargement...). L’absence de feedback peut créer une confusion pour l’utilisateur.

  • Amélioration de l’expérience utilisateur (UX) : Un retour visuel (message, icône, couleur...) permet à l’utilisateur de comprendre ce qui se passe, réduit l’incertitude et augmente la satisfaction. Afin de ne pas déstabliser l'utilisateur, les box d'état doivent être identiques d'une action à une autre.

Afin d'éviter le copier-coller, nous allons prévoir une fonction générique. Cela permettra :

  • Réduction du code redondant
  • Simplifie la maintenance et la mise à jour.

  • Standardisation et cohérence

Sur base du code suivant, créez une fonction générique qui affichera la box soit de succès soit de danger. Observez le code, quelles sont les parties communes ? Quels paramètres va recevoir la fonction ?

box alert
<?php 
if (!empty($message)) { ?>
    <div class="box-alert color-success">
        <?= nettoyage($message); ?>
    </div>
<?php
}

if (!empty($erreurs)) { ?>
    <div class="box-alert color-danger">
        <ul>
            <?php
            foreach ($erreurs as $erreur) {
                echo '<li>' . $erreur . '</li>';
            }
            ?>
        </ul>
    </div>
<?php } ?>
Exemple de solution
box
<?php 
/**
* Affiche un message sous forme d'alerte
* Gère aussi bien un message simple qu'un tableau d'erreurs sous forme de liste
* 
* @param string|array $message Contenu du message à afficher (chaîne ou tableau d'erreurs)
* @param string $type Type du message : 'success' ou 'danger' (par défaut)
*/
function afficherAlerte($message, $type = 'danger')
{
    if (!empty($message)) {
        echo '<div class="box-alert color-' . nettoyage($type) . '">';

        if (is_array($message)) {
            echo '<ul>';
            foreach ($message as $erreur) {
                echo '<li>' . nettoyage($erreur) . '</li>';
            }
            echo '</ul>';
        } else {
            echo nettoyage($message);
        }

        echo '</div>';
    }
}

Dashboard Admin

Afin de faciliter la gestion des articles, nous allons construire le dashboard de l'administrateur.

Observez la page gestion.php

Cette page liste tous les articles disponibles.

A vous d'utiliser la méthode adéquate pour afficher tous les titres des articles ainsi que leur id.

Supprimer un article

Lorsqu'on clique sur le bouton Supprimer, une demande de confirmation doit s'afficher.

deleteArticle

Si l'utilisateur confirme la suppression et que celle-ci se déroule correctement, une confirmation s'affiche et la liste des articles est mise à jour.

deleteArticle ok

Si un erreur est survenue, une alerte s'affiche.

deleteArticle nok

Méthode deleteArticle()

Question

Quelle est la requête sql utilisée pour supprimer l'article 13 ?

Soluce

DELETE FROM blog_article WHERE id = 13;

Il s'agit d'une requête paramètrée, nous allons donc suivre le même méthodologie que pour la méthode insertArticle()

A vous de créer la méthode.

Exemple de solution
deleteArticle()
 /**
 * Supprime un article
 *
 * @param String $id Identifiant de l'article à supprimer
 * @param string &$message Référence à une variable de message d'état
 * @return bool Retourne `true` si l'insertion est réussie, sinon `false`.
 *
 */
public static function deleteArticle($id, &$message)
{
    $noError = false;
    $bdd   = null;
    try {
        $bdd  = DBLink::connect2db(MYDB, $message);
        if (!$bdd) return $noError;
            $stmt = $bdd->prepare("DELETE FROM  " . self::TABLE_NAME . " WHERE id = :id_article");
        $stmt->bindValue(':id_article', $id);
        if ($stmt->execute()) {
            $noError = true;
        } else {
            $message .= 'Une erreur système est survenue.<br> 
                Veuillez essayer à nouveau plus tard ou contactez l\'administrateur du site. 
                (Code erreur: ' . $stmt->errorCode() . ')<br>';
        }
        $stmt = null;
    } catch (\Exception $e) {
        $message .= $e->getMessage() . '<br>';
    }
    DBLink::disconnect($bdd);
    return $noError;
}

Afficher la demande de confirmation

Comme montré par les captures d'écran précédentes, la box de confirmation s'affichera sur la même page.

Masquer la box de confirmation

Pour le moment, la box de confirmation est affichée.

Comment faire pour ne pas l'afficher dès que la page est appelée ?

Soluce

Utiliser un booleen.

Exemple
<?php 
$afficherConfirmation = false;

[...code...]

<?php if ($afficherConfirmation) { ?>
    <div class="modal">
        <div class="modal-content">

[...code...]

Modifier le lien "Supprimer"

La box est désormais cachée, il faut maintenant détecter son affichage. Pour rappel, elle doit s'afficher lorsque l'utilisateur clique sur le bouton "Supprimer". Pour cela, nous pouvons utiliser un paramètre get via le lien.

Exemple
<?php 
gestion.php?action=d&id=13
La page gestion.php est appelée avec les paramètres passés en get

  • action et la valeur d (pour delete)
  • id et la valeur représentant le numéro de l'article à supprimer

A vous de modifier le lien du bouton supprimer

Exemple de solution
a href="gestion.php?action=d&id=<?= $article->id; ?>" class="btn color-danger">Supprimer</a>

Détecter l'affichage de la box de confirmation

Lorsque la page est appelée, il faut

  • récupérer l'action et l'id s'ils existent.
Exemple
<?php 
$action = nettoyage($_GET['action'] ?? '');
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
  • tester si l'id est bien un numérique et si l'action vaut "d".

Pour rappel filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) :

  • Récupère et valide la valeur de id directement depuis la superglobale GET
  • Retourne un entier si la valeur est valide ou false en cas d'invalidité
  • Retourne null si la clé n'existe pas

L'affichage de la box de confirmation doit se faire uniquement si la condition est vraie.

Exemple
<?php 
if ($id !== false && $id !== null && $action === "d") {
    //traitement -> affichage de la box de confirmation
     $afficherConfirmation = true;
}

Récupérer l'id à supprimer

Rappelez-vous, le protocole HTPP est un protocole sans état (stateless). Ce qui signifie que chaque requête est indépendante et ne "se souvient" pas des requêtes précédentes. Cela peut poser un problème lorsqu'on affiche une boîte de confirmation pour supprimer un article. stateless >>

Prenons l'exemple suivant :

  • L'utilisateur clique sur le bouton "Supprimer" de l'article 13
  • La page gestion.php est appelée et reçoit en get l'id=13 et l'action=d (?action=d&id=123)
  • Si l'action vaut d et que l'id passé en get est valide, on affiche la box de confirmation
  • L'utilisateur clique sur le bouton pour confirmer la suppression
  • La page gestion est rechargée avec une requête POST. L'id initialement passé en GET est perdu.

A votre avis, comment conserver l'id et le récupérer lorsque l'utilisateur clique sur le bouton "Confirmer".

Soluce

Lors de l'affichage de la boîte de confirmation via un formulaire, on peut insérer l'id dans un <input type="hidden">. Cela permet de soumettre l'id via POST au moment de la confirmation.

Exemple
<?php 

    //affichage de la confirmation
    if ($id !== false && $id !== null && $action === "d") {
        $idToDelete =  $id;
        $afficherConfirmation = true;
    }

    [...code...]
    <form method="post" action='<?= htmlspecialchars($_SERVER["PHP_SELF"]); ?>'>
        <input type="hidden" name="id" value="<?= $idToDelete; ?>">
        <button type="submit" name="confirm-delete" class="btn color-danger">Oui, supprimer</button>
        <a href="<?= htmlspecialchars($_SERVER["PHP_SELF"]); ?>" class="btn color-theme">Annuler</a>
    </form>

Suppression de l'article

Comment détecter que l'utilisateur a cliqué sur le bouton de confirmation ?

Soluce
<?php 
if (isset($_POST['confirm-delete']) && $idToDelete) {
    //traitement
}

Quelles sont les étapes à réaliser ?

Soluce
  • Masquer la box de confirmation
  • Appeler la méthode deleteArticle()
  • Si la suppression est fonctionnelle
    • créer un message de confirmation
    • recharger la liste des articles
  • Sinon, créer un message d'erreur

A vous de créer le code.

Exemple de solution
<?php 
// Suppression de l'article si confirmé
if (isset($_POST['confirm-delete']) && $idToDelete) {
    $afficherConfirmation = false;
    if ($articleRepository->deleteArticle($idToDelete, $messageErreur)) {
        $message = "L'article a bien été supprimé.";
        $articles = $articleRepository->getAllArticles($messageErreur);
    } else {
        $messageErreur .= "Erreur lors de la suppression de l'article.";
    }
}

Modifier un article

Lorsque l'utilisateur cliquer sur le bouton "Modifier", le formulaire doit contenir les données de l'article à modifier.

Vous pouvez soit utiliser une nouvelle page soit utiliser la page qui permet d'ajouter un nouvel article. Afin d'éviter les redondances inutiles, c'est cette deuxième option que nous allons réaliser.

Méthode updateArticle()

Question

Quelle est la requête sql utilisée pour modifier le titre de l'article 13 ?

Soluce
<?php 
UPDATE blog_article SET titre = "nouveau titre", contenu = "nouveau contenu" WHERE id = 13;

Il s'agit d'une requête paramètrée, nous allons donc suivre le même méthodologie que pour la méthode deleteArticle()

A vous de créer la méthode.

Exemple de solution
<?php 
 /**
 * Modifie un article
 *
 * @param Article $article Objet représentant l'article à modifier
 * @param string &$message Référence à une variable de message d'état
 * @return bool Retourne `true` si la modification est réussie, sinon `false`.
 *
 */
public static function updateArticle($article, &$message)
{
    $noError = false;
    $bdd   = null;
    try {
        $bdd  = DBLink::connect2db(MYDB, $message);
        if (!$bdd) return $noError;
        $stmt = $bdd->prepare("UPDATE " . self::TABLE_NAME . " SET titre = :titre, contenu = :contenu WHERE id = :id_article");
        $stmt->bindValue(':id_article', $article->id);
        $stmt->bindValue(':titre', $article->titre);
        $stmt->bindValue(':contenu', $article->contenu);
        if ($stmt->execute()) {
            $noError = true;
        } else {
            $message .= 'Une erreur système est survenue.<br> 
                Veuillez essayer à nouveau plus tard ou contactez l\'administrateur du site. 
                (Code erreur: ' . $stmt->errorCode() . ')<br>';
        }
        $stmt = null;
    } catch (\Exception $e) {
        $message .= $e->getMessage() . '<br>';
    }
    DBLink::disconnect($bdd);
    return $noError;
}

Modifier le lien "Modifier"

Lorsque l'utilisateur clique sur le bouton "Modifier", la page new.php doit s'afficher et recevoir l'id de l'article à modifier.

Exemple
<?php 
new.php?id=73

A vous de modifier le lien du bouton "modifier"

Exemple de solution
Exemple de solution
<?php 
<a href="new.php?id=<?= $article->id; ?>" class="btn color-theme">Modifier</a>

Détecter et afficher

La page new.php doit détecter si un paramètre id est passé en get.

<?php 
// Récupération de l'ID en GET
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);

A votre avis, quelles sont les étapes à prévoir si id est différent de null et faux ?

Soluce
  • récupérer les valeurs de l'article
  • si un article est retourné, l'afficher dans le formulaire

A vous de créer le code.

Exemple de solution
<?php 
//si modification
if ($id !== false && $id !== null) {
    // Récupération des valeurs de l'article 
    $articleRepository = new ArticleRepository();
    $article = $articleRepository->getArticleById($id, $messageErreur);
    //si l'article existe
    if ($article) {
        //assigner les valeurs aux variables qui seront affichées dans le formulaire    
        $titre = nettoyage($article->titre);
        $contenu = nettoyage($article->contenu);
    } else {
        $messageErreur = "Article introuvable.";
    }
}

Adapter l'affichage

Pour le moment, le titre de la page est Nouvel Article. Nous allons l'adapter pour qu'il afficher soit Nouvel Article soit Modifier Article.

A votre avis, comment faire ?

Soluce
<?php 
<h1><?= $id ? 'Modifier' : 'Nouvel' ?> Article</h1>

Faites de même pour le titre de niveau 2 : <h2> et la valeur du bouton de soumission du formulaire : <input type="submit"

Récupérer l'id de l'article

Prenons l'exemple suivant :

  • à partir de la page gestion, l'utilisateur clique sur le bouton "Modifier" de l'article 18
  • l'id=18 est envoyé à la page new.php
  • la page détecte cet id, récupère le titre et le contenu de l'article 18 et les affiche dans le formulaire
  • l'utilisateur clique sur le bouton "Modifier"
  • la page new.php doit détecter la soumission et modifier l'article 18

Mais rappelez-vous : le protocole HTTP est stateless. Au moment où l'utilisateur clique sur le bouton "Modifier", l'id de l'article est perdu. Nous allons donc le cacher dans le formulaire de modification afin de le récupérer. (même procédé que pour la suppression).

A vous de prévoir le champ caché dans le formulaire.

Exemple de solution
<?php 
<input type="hidden" name="id" value="<?= $id ?>">

Détecter la soumission

Actuellement, le code permet de détecter la soumission du formulaire et d'insérer un nouvel article. Nous allons l'adapter : il faut détecter si nous devons réaliser un ajout ou une modification.

Sur quoi pouvons-nous baser ?

Soluce

Si un id est présent et comprend une valeur

<?php 
$id = filter_input(INPUT_POST, 'id', FILTER_VALIDATE_INT);

Modifier le code pour réaliser la modification si l'id est présent.

Soluce
<?php 
if (empty($erreurs)) {
    $article = new Article();
    $article->titre = $titre;
    $article->contenu = $contenu;
    $articleRepository = new ArticleRepository();

    if ($id !== false && $id !== null) {
        // Mode modification
        $article->id = $id;
        if ($articleRepository->updateArticle($article, $messageErreur)) {
            $message = "Article mis à jour avec succès.";
        } else {
            $erreurs[] = "Erreur technique lors de la mise à jour.";
            $erreurs[] = $messageErreur;
        }
    } else {
        // Mode ajout
        if ($articleRepository->insertArticle($post, $messageErreur)) {
            $message .= "Article correctement ajouté.";
            $titre = $contenu = '';
        } else {
            $erreurs[]  = "Erreur technique. Veuillez contacter l'administrateur.";
            $erreurs[] = $messageErreur;
        }
    }
}

Refactoring

Actuellement, les vues (pages d'affichage) contiennent une quantité importante de code PHP, ce qui nuit à la lisibilité et à la maintenance du projet.

Pour améliorer la structuration du code :

  • Évitez d’intégrer la logique métier dans les vues et limitez leur rôle au rendu de l’interface
  • Déplacez les traitements vers des fonctions dédiées dans des fichiers distincts
  • Organisez le projet de manière modulaire en séparant clairement les responsabilités

Si vous maîtrisez les concepts vu jusqu'à maintenant, vous pouvez aller plus loin en adoptant une architecture plus structurée :

  • Modèle : Centralise la gestion des données et les requêtes SQL.
  • Vue : Se limite à l'affichage des données.
  • Contrôleur : Reçoit la requête utilisateur, traite les données via les modèles et les transmet à la vue.

Cette approche simplifie la gestion du code et préfigure une architecture de type MVC, que vous étudierez en détail au Bloc 2