logo du langage PHP Les bases du langage PHP

Chapitre 21 - Mise en application

La vocation de ce dernier chapitre est d'exposer le résultat de la mise en application du contenu de ce cours condensé !

Sous forme de captures d'écran, brièvement commentées, d'une simulation de E Boutique

Codée en PHP procedural, avec seulement les bases présentées ici. La syntaxe, le code seront plus complexes, poussés, combinés (notamment lorsque le sujet n'était qu'effleuré, comme pour la securité )

Avec toujours Bootstrap pour l'intégralité de la mise en page

La barre de navigation, qui offre au visiteur non connecté la possibilité de s'incrire. Et s'il l'était déjà , de se connecter. Il pourra aussi visualiser son panier, mais pour pouvoir procéder au paiement, il devra néanmoins se connecter

Notez que les sessions pour le panier et le user sont indépendantes l'une de l'autre. Le panier sera alimenté, dynamique, éditable par le visiteur, même s'il ne s'est pas encore connecté (et la session donc pas encore crée ou récupérée)

Une modale basique de Bootstrap qui accueillera le formulaire de connexion. Elle offre une interface plus "moderne" qu'une page web lambda (pour un même script PHP, à la virgule près).

Le formulaire d'inscription en revanche sera affiché sur une pleine page. Le nombre de champs à renseigner étant plus important

Désormais, mon utilisateur, connecté, ne bénéficie plus de la même barre de navigation.

Exit les onglets inscription et connexion, qui n'ont plus de raison d'etre. Apparition en revanche de l'onglet profil (vers lequel il sera automatiquement redirigé après avoir réussi sa connexion) ainsi que d'un bouton de déconnexion

Apparition aussi d'un onglet admin, pour accéder au back-office, a condition que cet utilisateur en ait les droits. Sinon, il lui restera invisible. Inaccessible aussi via l'URL, même s'il tente d'en deviner le chemin. Une redirection automatique aura été mise en place à cet effet !

Le back-office est une interface essentielle pour gérer la base de données à partir du site

Je ne pourrai livrer un site (relié a une BDD) à son commanditaire sans cette fonctionnalité (E Commerce ou non) !

Je ne pourrai le laisser gérer son commerce via l'interface de phpMyAdmin. Cela sera trop compliqué pour lui et donc peu professionnel de ma part

A noter que la mise en page a été réalisée grace a un template de Bootstrap (offrant par exemple la possibilité de réduire le menu latéral). C'est toujours un petit +, qui permet de bien différencier cet aspect du site avec la partie publique.

CRUD est l'acronyme de Create/ajouter, Read/lire (ou voir), Update/modifier et Delete/supprimer

Et c'est en cela qu'un back-office doit etre mis a disposition, mais aussi protégé de tout regard/intervention extérieur !

A partir de cette interface, l'administrateur du site pourra interagir avec la base de données. Dans le cadre d'un E Commerce, il pourra gérer le stock de ses produits. Modifier leur prix. Ajouter une nouvelle référence à son catalogue. En supprimer une autre qu'il ne va plus commercialiser etc...

Dans le même ordre d'idée, il pourra gérer ses clients, leur appliquer des avantages selon leur fidèlité, vérifier l'état des commandes etc... Et il en sera de même pour un forum de discussions, un site d'information, d'une bibliothèque etc...Bref, le back-office/CRUD fait partie des incontournables !

L'interface/tableau ci-dessous est dédiée à la gestion des différents articles commercialisés. En plus d'afficher l'intégralité des infos les concernant, d'indiquer leur nombre total (en haut de page), et de fluidifier la navigation en n'affichant que dix produits par page (pagination a droite du tableau). J'ai donc ajouté les fonctionnalité pour ajouter, modifier ou supprimer un article

Pour l'ajout, je n'ai eu qu'a mettre en place un formulaire avec la requete SQL INSERT INTO qui va avec. Concernant la modification et la suppression, en plus des bonnes requetes SQL ( UPDATE et DELETE ), je suis passé par la methode GET

De la même manière que j'ai récupéré les infos concernant le produit pour les afficher dans mon tableau, je vais me servir de l'Id du produit pour le cibler dans l'attribut href de ma balise <a>. Balise qui entourera les icones crayon et poubelle, respectivement pour modifier ou supprimer un article en particulier (dans la colonne Actions)

<td><a href="?action=delete&id_produit=<?= $produit['id_produit'] ?>">
<i class="bi bi-trash-fill text-danger"></i></a></td>

Avec le ?action=delete suivi de la récupération de l'id du produit ( &id_produit=<?= $produit['id_produit'] ) ci-dessus, je peux derrière scripter la condition. Si il existe dans l'URL une action et que cette dernière est égale égale à delete , alors, je fais une requete SQL query pour supprimer le produit qui possède l' id_produit récupéré dans l'URL


if(isset($_GET['action']) && $_GET['action'] == "delete"){
  $pdo->query("DELETE FROM produit WHERE id_produit = $_GET[id_produit] ;");
}

Noter que si pour delete j'utilise query, pour modifier les données d'un produit, d'un utilisateur, d'une commande etc... je passerai par une requete préparée (voir le chapitre sur la securité ). Il en sera de même pour l'ajout d'un produit, d'un user ...

Si pour le back-office j'ai procédé à un affichage dans un tableau (pour des raisons pratiques), il n'en sera pas de même pour l'interface mise à la disposition des visiteurs de mon site.

Je vais tout d'abord mettre en place deux barres de navigations, une horizontale, pour une recherche par publics concernés. L'autre latérale, qui va détailler mes produits, rangés par catégories

Dans les deux cas, je fais un SELECT DISTINCT, sinon, le navigateur affichera autant de fois la catégorie (ou le public) qu'il existe de produits


$afficheCategories = $pdo->query("SELECT DISTINCT categorie FROM produit ORDER BY categorie ASC");

Suivi de cette boucle pour les afficher


<div class="list-group text-center">
  <?php while($categorie = $afficheCategories->fetch(PDO::FETCH_ASSOC)): ?>
    <a class="btn btn-outline-success my-2" href="?categorie=<?= $categorie['categorie'] ?>">
    <?= $categorie['categorie'] ?> </a>
  <?php endwhile; ?>
</div>

Remarque

Hormis bien sur la while pour les génerer, chaque catégorie récupérée avec $categorie['categorie'] sera "prise" dans une balise <a> dont l'attribut href enverra dans l'URL le nom de cette même catégorie : ?categorie=<?= $categorie['categorie'] ?>


Pour l'affichage des articles à proprement parlé, hormis la mise en place d'une pagination (je ne veux que trois produits par page), je vais aussi faire un SELECT mais sans DISTINCT


$afficheProduits = $pdo->query("SELECT * FROM produit WHERE categorie = '$_GET[categorie]'
ORDER BY prix ASC ");

Suivi de la boucle


while($produit = $afficheProduits->fetch(PDO::FETCH_ASSOC)){
  ... code ...
}

Dans le bouton "voir produit", je vais a nouveau me servir de la méthode GET pour envoyer dans l'URL l'id du produit, et ainsi récupérer les infos le concernant dans une nouvelle page (gérée par le fichier fiche_produit.php)


<a href="fiche_produit.php?id_produit=<?= $produit['id_produit'] ?>" class="btn btn-outline-success">
<i class='bi bi-search'></i> Voir Produit</a>

Je souligne l'extrème utilité de la méthode GET durant les deux derniers sous-chapitres. Pour modifier/supprimer un article dans la base de données. Comme pour récupérer les infos d'un produit qui m'interesse, d'une page générale, vers sa fiche individuelle

Une fois donc cliqué sur le bouton, l'id du produit sera envoyé dans l'URL, et récupéré pour afficher l'article qui m'interesse


if(isset($_GET['id_produit'])){
  $afficheProduit = $pdo->query("SELECT * FROM produit WHERE id_produit = '$_GET[id_produit]' ;");
  $produit = $afficheProduit->fetch(PDO::FETCH_ASSOC);
}

A nouveau, dans un premier temps, je vérifie avec isset si une valeur existe, a bien été envoyée dans l'URL, qui corresponderait a un id_produit

Si cette condition est remplie, alors avec query puis select, je demande à récupérer toutes les données liées au produit dont l'id correspond avec celui envoyé dans l'URL

Je vais afficher certaines données dans un cadre que j'ai défini (ici dans un <h2>, puis une card pour la photo, le prix etc...). Et d'autres dans un <form> car je vais devoir envoyer la quantité désirée par l'utilisateur (dans un <select> ) vers le panier


<form method="POST" action="panier.php">
<?php if($produit['stock'] > 0): ?>
  <input type="hidden" name="id_produit" value="<?= $produit['id_produit'] ?>">
  <label for="">J'en achète</label>
  <select class="form-control col-md-5 mx-auto" name="quantite" id="quantite">
    <?php for($i = 1; $i <= min($produit['stock'],5); $i++): ?>
    <option class="bg-dark text-light" value="<?= $i ?>"><?= $i ?></option>
    <?php endfor; ?>
  </select>
    <button type="submit" class="btn btn-outline-success my-2" name="ajout_panier">
    <i class="bi bi-plus-circle"></i> Panier <i class="bi bi-cart3"></i></button>
<?php else: ?>
    <p class="card-text"><div class="badge badge-danger text-wrap p-3">
    Produit en rupture de stock</div></p>
<?php endif; ?>
</form>

Le formulaire détaillé :

Tout d'abord, j'indique dans l'attribut action (de ma balise <form> ) que je vais envoyer toutes les données collectées, dans la page gérée par le fichier panier.php

En second lieu, je mets un <input type="hidden" name="id_produit" value="<?= $produit['id_produit'] ?>"> qui me sera nécessaire pour récupérer la quantité demandée par l'utilisateur. Je ferai donc transiter en même temps que cette dernière, la donnée relative à l'id (mais qui sera cachée/invisible dans ma page)

Enfin, je vais vérifier qu'il me reste du stock, sinon, j'en avertirai le user, sans lui laisser la possibilité de selectionner une quantité

Et, si j'ai du stock, je vais limiter le nombre à 5 (dans le cas où le stock dépasse ce chiffre => $i <= min($produit['stock'],5) ). Je ne le fais que dans ce cas de figure particulier, pas si le site s'adresse à des détaillants professionnels !

Comme toujours, au préalable, je vais vérifier avec isset (ou array_key_exists) que j'ai bien reçu une donnée (en POST, ['ajout_panier'] étant le name du bouton de validation du formulaire)


if(isset($_POST['ajout_panier'])){
  $detailPanier = $pdo->query("SELECT * FROM produit WHERE id_produit = '$_POST[id_produit]' ;");
  $detail = $detailPanier->fetch(PDO::FETCH_ASSOC);
  ajouterAuPanier($detail['id_produit'], $detail['categorie'], $detail['titre'], $detail['photo'],
  $_POST['quantite'], $detail['prix']);
}

Une fois cette condition remplie, je selectionne toutes les données de l'article dont l'id sera similaire à celui reçu via le formulaire (grace à l'input de type hidden !)

Ces valeurs, extraites de la BDD, vont aller alimenter la session panier, créee (ou récupérée si elle existe déjà) par la fonction ajouterAuPanier (fonction scriptée dans un fichier fonction.php, inclu grace à require_once , voir chapitre 12 sur les inclusions de fichiers). Une seule donnée ne proviendra pas de la BDD, c'est la quantité du produit. Elle arrive directement du formulaire mis en place dans la fiche du produit

L'affichage des informations pour les différents produits (s'il y en a plus d'un) se fera grace à une boucle for


for($i = 0; $i < count($_SESSION['panier']['id_produit']); $i++){
  ... code ...
}

La fonction ajouterAuPanier qui gère la session a crée un array imbriqué, d'où le $_SESSION['panier']['id_produit'] dans la syntaxe ci-dessus. Et count va permettre de créer autant de cards qu'il y a d'id_produit dans la session dédiée au panier

Je ne détaille pas le code de cette fonction, d'autant plus qu'elle est couplée à deux autres : creerPanier et retirerDuPanier, aussi scriptées dans fonction.php

J'offre la possibilité à l'utilisateur d'apporter diverses corrections à sa commande avant de la valider. Hormis continuer ses achats, il peut modifier la quantité désirée


if(isset($_POST['ajouter'])){
  for($i = 0; $i < count($_SESSION['panier']['id_produit']); $i++){
    if($_SESSION['panier']['id_produit'][$i] == $_POST['id_produit']){
      $_SESSION['panier']['quantite'][$i] += 1;
    }
  }
}

En premier, je vérifie que je reçois bien une donnée du formulaire dont le bouton à pour name ajouter. La boucle for qui suit, me permet de passer en revue tous les articles présent dans ma session, et ainsi faire correspondre le bon id_produit avec celui reçu via mon formulaire ( $_SESSION['panier']['id_produit'][$i] == $_POST['id_produit'] ). Une fois identifié, j'ajoute à sa quantité + 1 $_SESSION['panier']['quantite'][$i] += 1 ( voir le chapitre 5 sur les opérateurs arithmétiques ). La procédure est similaire pour diminuer la quantité

Le user peut aussi décider de retirer un article de son panier


if(isset($_GET['action']) && $_GET['action'] == "delete"){
  retirerDuPanier($_GET['id_produit']);
}

Il me suffit pour cela de faire transiter dans l'URL ?action=delete ainsi que l'id du produit, puis de faire intervenir la fonction retirerDuPanier


Au moment où l'utilisateur décidera de valider son panier, il me faudra vérifier si la quantité commandée n'est pas inférieure à mon stock. Entre le moment où il a selectionné le produit et le moment où il procédera au paiement, du temps aura pu s'écouler

Voici le script pour cette vérification (sans la boucle for) dans lequel j'envisage deux cas de figure. Je ne peux lui en vendre autant qu'il désire, mais tout de même un peu. Ou alors rien, car mon stock est à zéro


if($produit['stock'] < $_SESSION['panier']['quantite'][$i]){
  if($produit['stock'] > 0){
    $_SESSION['panier']['quantite'][$i] = $produit['stock'];
    $content .= '<div class="alert alert-danger" role="alert">La quantité du produit ' 
        . $_SESSION['panier']['categorie'][$i] . " " . $_SESSION['panier']['titre'][$i] .
        ' a été diminuée. Vérifiez votre nouveau panier !</div>';
      }else{
        $content .= '<div class="alert alert-danger" role="alert">Le produit ' .
        $_SESSION['panier']['categorie'][$i] . " " . $_SESSION['panier']['titre'][$i] . 
         ' a été retiré de votre panier car il est désormais en rupture de stock </div>';
         retirerDuPanier($_SESSION['panier']['id_produit'][$i]);
         $i--;
      }
    }

Dans le premier cas, je corrige la quantité avec ce qui reste de disponble, tout en demandant à valider la nouvelle quantité (pour etre sur que cela lui convient toujours). Dans le second cas, je l'informe de la rupture de stock, puis procède au retrait du produit dans son panier. L'ordre est important , car si je retire le produit avant de notifier le message, j'aurais une erreur PHP (l'id_produit n'existant désormais plus dans mon panier). Le $i-- décrémente, pour compenser cet effet de produit supprimé

Une fois validé le panier, il restera à entrer en BDD la nouvelle commande (dans la table commande). Elle comportera l'id du user, récupéré via sa session user. Le montant total de la vente sera lui connu grace à la fonction utilisateur $montantTotal, qui additionne le montant de tous les articles du panier. La fonction prédéfinie NOW() renseignant sur la date précise à laquelle se déroule la transaction. Cette entrée en BDD permettra, entre autres, à l'administrateur du site de surveiller l'état de la livraison


$ajouterCommande = $pdo->prepare("INSERT INTO commande (id_membre, montant, date_enregistrement)
VALUES (:id_membre, :montant, NOW()) ;");
$ajouterCommande->bindValue(':id_membre', $_SESSION['membre']['id_membre'], PDO::PARAM_INT);
$ajouterCommande->bindValue(':montant', montantTotal(), PDO::PARAM_INT);
$ajouterCommande->execute();

Immédiatement après, j'insère en BDD (dans la table details_commande) le détail de la transaction, pour chaque article vendu dans la vente globale ( l'id de cette dernière, dont-il fait partie, l'id du produit, son prix ainsi que la quantité)

Je dispose de toutes ses informations avec ma session panier, hormis l'id de la commande "parent". Je vais l'obtenir en faisant intervenir la fonction prédéfinie de la classe PDO : lastInserId ($id_commande = $pdo->lastInsertId()), valeur que j'affecte à la variable id_commande


$id_commande = $pdo->lastInsertId();
for($i = 0; $i < count($_SESSION['panier']['id_produit']); $i++){
  $ajouterDetailCommande = $pdo->query("INSERT INTO details_commande (id_commande, id_produit,
  quantite, prix_unite) VALUES (". $id_commande .",". $_SESSION['panier']['id_produit'][$i] .
  ",". $_SESSION['panier']['quantite'][$i] .",". $_SESSION['panier']['prix'][$i] .") ;");
    }

Toujours dans la foulée, je déduis la quantité de chaque produit vendu de mon stock, de manière à ce qu'il soit à jour instantanément pour la prochaine transaction


$modifierStock = $pdo->query("UPDATE produit SET stock = stock - ". $_SESSION['panier']['quantite']
[$i] . " WHERE id_produit = ". $_SESSION['panier']['id_produit'][$i] ." ;");

Enfin, après avoir validé l'intégralité de la procédure, je vide la session panier


unset($_SESSION['panier']);

En conclusion

Cette simulation de boutique n'est pas totalement terminée. Je n'ai par exemple pas crée de cookie pour que les sessions user ou panier perdurent dans le temps, après fermeture du navigateur

Je n'ai pas non plus mis en place la fonctionnalité qui permettrait à l'utilisateur de modifier dans son espace personnel, son profil. Cela serait aisé et rapide, car c'est déjà codé dans le CRUD (sous-chapitre 21.5)

Il resterait aussi à mettre en place un système pour réinitialiser son mot de passe en cas d'oubli de ce dernier. Mais avec le b.a.-ba du PHP exposé dans ce support de cours condensé, je peux déjà "batir" une simulation de boutique assez aboutie

Chapitre précédent