Aller au contenu principal

5) Interactions

Formulaires et actions de jeu

Séquence des tours de jeu

Objectifs de la séance

  • Comprendre le cycle requête/réponse et comment un formulaire HTML déclenche du code PHP.
  • Écrire des formulaires HTML utilisant la méthode POST.
  • Lire et valider les données envoyées par un formulaire avec $_POST et isset().
  • Convertir les données reçues avec intval() et htmlspecialchars().
  • Créer la classe Jeu qui centralise la logique du jeu et fait le lien entre l'interface et la base de données.
  • Utiliser la syntaxe alternative PHP (if(): / endif;) dans les templates HTML.
  • Sauvegarder l'état du jeu en base après chaque action du joueur.

Notions théoriques

Le cycle d'une interaction avec un formulaire

Jusqu'ici, index.php s'exécutait de la même manière à chaque requête : il chargeait les personnages, affichait la page, et s'arrêtait.

Le joueur n'avait aucun moyen d'agir.

Avec un formulaire, le cycle devient le suivant :

  1. Le navigateur envoie une requête GET vers index.php (premier chargement).
  2. PHP génère la page HTML avec le formulaire et l'envoie au navigateur.
  3. L'utilisateur remplit le formulaire et clique sur le bouton.
  4. Le navigateur envoie une requête POST vers index.php, en joignant les données du formulaire.
  5. PHP reçoit ces données dans le tableau $_POST, les traite (déplacement, attaque...), sauvegarde les changements en base, puis génère à nouveau la page HTML mise à jour.

À chaque clic sur un bouton, on recommence à partir de l'étape 4.

info

C'est ce cycle répété qui permet de jouer.

La méthode POST

Un formulaire HTML peut envoyer ses données de deux manières :

  • GET : les données apparaissent dans l'URL (?nom=Lara&vie=100). Utile pour des recherches ou des filtres, mais inadapté pour des actions qui modifient des données.
  • POST : les données sont envoyées dans le corps de la requête HTTP, invisibles dans l'URL. C'est la méthode à utiliser pour toute action qui modifie l'état du jeu.
<form method="POST" action="index.php">
<input type="text" name="nom" />
<input type="submit" value="Envoyer" />
</form>

En PHP, les données envoyées en POST sont accessibles via le tableau superglobal $_POST :

if (isset($_POST['nom'])) {
$nom = $_POST['nom'];
}

$_POST est un tableau associatif dont les clés correspondent aux attributs name des champs du formulaire.

Vérifier et nettoyer les données reçues

Toute donnée reçue depuis l'extérieur (formulaire, URL, cookie...) doit être traitée avec méfiance. Deux règles minimales à respecter systématiquement :

1. Vérifier l'existence avec isset()

isset($variable) renvoie true si la variable existe et n'est pas null. Sans cette vérification, accéder à $_POST['action'] quand aucun formulaire n'a été soumis provoquerait un avertissement PHP (notice : undefined index).

if (isset($_POST['action'])) {
$action = $_POST['action'];
// traiter l'action
}

2. Convertir le type avec intval()

Toutes les valeurs de $_POST sont des chaînes de caractères, même quand elles représentent des nombres. Si un champ <input type="number" name="vie"> renvoie "100", PHP voit une chaîne, pas un entier. La fonction intval() convertit une chaîne en entier :

$vie = intval($_POST['vie']); // "100" devient 100

3. Neutraliser le HTML avec htmlspecialchars()

Si une valeur textuelle saisie par l'utilisateur est réaffichée telle quelle dans la page, un utilisateur malveillant pourrait y injecter des balises HTML ou du JavaScript (attaque XSS). htmlspecialchars() convertit les caractères spéciaux en entités HTML :

$nom = htmlspecialchars($_POST['nom']);
// "<script>" devient "&lt;script&gt;" et s'affiche comme du texte

La syntaxe alternative PHP dans les templates

Quand on mélange PHP et HTML (dans la partie affichage d'une page), les accolades {} peuvent rendre le code difficile à lire. PHP propose une syntaxe alternative plus lisible :

Syntaxe classiqueSyntaxe alternative
if (...) {if (...):
}endif;
foreach (... as ...) {foreach (... as ...):
}endforeach;
<?php if (count($personnages) > 0): ?>
<ul>
<?php foreach ($personnages as $p): ?>
<li><?php print $p->getNom(); ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>

Cette syntaxe est préférable dans les templates (fichiers qui mélangent HTML et PHP) car elle rend immédiatement visible à quoi correspond chaque endif ou endforeach, sans avoir à chercher l'accolade ouvrante correspondante.

Le champ hidden pour identifier l'action

Quand une même page contient plusieurs formulaires (créer un personnage, déplacer, attaquer...), il faut distinguer lequel a été soumis. La solution standard est d'ajouter un champ caché (type="hidden") dans chaque formulaire, avec une valeur identifiant l'action :

<form method="POST" action="index.php">
<input type="hidden" name="action" value="deplacer" />
<!-- ... autres champs ... -->
</form>

Côté PHP, on lit $_POST['action'] pour savoir quelle action traiter.

Le rôle de la classe Jeu

À ce stade, index.php orchestre tout : il charge les personnages, traite les actions, sauvegarde, et affiche. Plus le jeu gagne en fonctionnalités, plus ce fichier devient ingérable.

La classe Jeu va centraliser toute la logique métier :

  • charger et sauvegarder la partie via Database ;
  • valider et exécuter les actions (déplacer, attaquer, vérifier les limites du plateau) ;
  • exposer les données nécessaires à l'affichage.

index.php ne fera plus que lire les données de $_POST, déléguer le traitement à Jeu, puis afficher le résultat. Ce découpage est essentiel pour maintenir un code lisible quand la complexité augmente.


Point de départ du TP

Ce TP reprend le projet issu de la séance 4.

Le dossier de notre projet contient actuellement les fichiers suivants :

  • Personnage.php,
  • Guerrier.php,
  • Mage.php,
  • Database.php,
  • index.php
  • et jeu.db.

À la fin de cette séance, notre projet contiendra un fichier supplémentaire :

FichierStatutDescription
Personnage.phpinchangéclasse abstraite parente
Guerrier.phpinchangéclasse fille guerrier
Mage.phpinchangéclasse fille mage
Database.phpinchangégestion SQLite
Jeu.phpnouveaulogique centrale du jeu
index.phprefondutraitement des formulaires POST + affichage
jeu.dbexistantfichier de base de données SQLite

Test de mémorisation/compréhension


Pour quelle raison la méthode GET est-elle inadaptée pour une action qui modifie l'état du jeu ?


Que renvoie la fonction `isset()` si la variable testée existe mais contient la valeur `null` ?


Quel type précis d'avertissement PHP cherche-t-on à éviter en utilisant `isset()` avant d'accéder à `$_POST['action']` ?


Pourquoi est-il indispensable d'utiliser `intval()` sur une valeur numérique provenant de `$_POST` ?


Quelle est la transformation exacte opérée par `htmlspecialchars()` sur la chaîne `"<script>"` ?


Quel est le nom de l'attaque que l'on évite en utilisant `htmlspecialchars()` ?


Quelle règle à suivre concernant le traitement de toute donnée reçue depuis l'extérieur (formulaire, URL, cookie...) ?


Quelle est la conséquence du fait que `index.php` gère tout (chargement, actions, sauvegarde, affichage) avant l'introduction de la classe `Jeu` ?


Parmi les tâches suivantes, laquelle ne fait PAS partie des responsabilités de la classe `Jeu` ?


Après la refonte de l'architecture du jeu, quel est le rôle exact dévolu au fichier `index.php` ?


Pourquoi le découpage des responsabilités entre `index.php` et la classe `Jeu` est essentiel ?


Si un développeur oublie d'utiliser `htmlspecialchars()` sur une variable `$_POST['nom']` avant de l'afficher avec `print`, quel risque encourt-il ?



TP pour réfléchir et résoudre des problèmes

Rendre le jeu interactif avec des formulaires POST

Étape 1 - Comprendre ce que PHP reçoit quand un formulaire est soumis

Avant de traiter les données d'un formulaire, il est utile de comprendre exactement ce que PHP reçoit.

Modifier index.php pour y ajouter un petit formulaire de test, et afficher le contenu de $_POST à chaque soumission :

<form method="POST" action="index.php">
<label for="test_nom">Nom :</label>
<input type="text" name="test_nom" id="test_nom" />
<label for="test_vie">Vie :</label>
<input type="number" name="test_vie" id="test_vie" value="100" />
<input type="submit" value="Tester" />
</form>

Ajouter en haut du fichier PHP, avant tout autre traitement :

<?php
if (isset($_POST['test_nom'])) {
print "<pre>";
var_dump($_POST);
print "</pre>";
}
?>

Soumettre le formulaire avec différentes valeurs. Observer :

  • que les clés du tableau $_POST correspondent aux attributs name des champs ;
  • que toutes les valeurs sont des chaînes de caractères (string), même le champ numérique ;
  • ce qui se passe quand on soumet sans remplir un champ.

Ce formulaire de test peut être supprimé à la fin de cette étape.


Bonne pratique - debug

Avant de développer le traitement d'un formulaire, Il est conseillé de toujours vérifier ce que PHP reçoit avec var_dump($_POST). C'est le moyen le plus rapide d'identifier des erreurs de nommage de champs ou des problèmes de type.

Ne jamais supposer la structure de $_POST sans l'avoir observée.

Étape 2 - Créer la structure de la classe Jeu

Créer un nouveau fichier Jeu.php.

La classe Jeu doit :

  • inclure Database.php, Guerrier.php et Mage.php avec require_once ;
  • avoir trois propriétés privées : $personnages (tableau), $database (objet Database), $taillePlateau (entier) ;
  • dans le constructeur, instancier Database, initialiser $personnages à un tableau vide [], et $taillePlateau à 5 ;
  • exposer trois méthodes publiques d'accès : getPersonnages(), getTaillePlateau() et getDatabase().

Ne pas encore écrire les méthodes de logique métier (chargerPartie, creerPersonnage, etc.) : on les ajoutera aux étapes suivantes.

Tester dans index.php que la classe s'instancie sans erreur :

require_once "Jeu.php";
$jeu = new Jeu();
print "<p>Taille du plateau : " . $jeu->getTaillePlateau() . "</p>";

Bonne pratique - Développement incrémental

Il est conseillé de construire une classe par étapes successives plutôt que de tout écrire d'un coup. Tester après chaque ajout garantit que les erreurs sont détectées immédiatement, au moment où on les introduit, et non après avoir accumulé des dizaines de lignes.

Étape 3 - Ajouter chargerPartie à la classe Jeu

Ajouter dans Jeu une méthode publique chargerPartie().

Elle délègue simplement la lecture à $this->database->chargerPersonnages() et stocke le résultat dans $this->personnages.

Modifier index.php pour utiliser cette méthode et afficher les personnages chargés :

$jeu = new Jeu();
$jeu->chargerPartie();
$personnages = $jeu->getPersonnages();
// afficher les personnages...

Si la base est vide (premier lancement), le tableau sera vide. Ce cas sera géré à l'étape suivante.

Étape 4 - Ajouter le premier formulaire : créer un personnage

On va maintenant ajouter le premier formulaire jouable. Il permettra à l'utilisateur de créer un personnage en choisissant son nom, son type (guerrier ou mage), ses points de vie et ses dégâts.

Dans la partie HTML de index.php, ajouter le formulaire suivant :

<h2>Créer un personnage</h2>
<form method="POST" action="index.php">
<input type="hidden" name="action" value="creer" />

<label for="nom">Nom :</label>
<input type="text" name="nom" id="nom" required />
<br/>

<label for="type">Type :</label>
<select name="type" id="type">
<option value="guerrier">Guerrier</option>
<option value="mage">Mage</option>
</select>
<br/>

<label for="vie">Points de vie :</label>
<input type="number" name="vie" id="vie" value="100" min="1" required />
<br/>

<label for="dps">Dégâts par tour :</label>
<input type="number" name="dps" id="dps" value="10" min="1" required />
<br/>

<input type="submit" value="Créer le personnage" />
</form>

En haut du fichier PHP (avant le HTML), ajouter le traitement de base : détecter si le formulaire a été soumis et afficher les valeurs reçues pour l'instant.

<?php
require_once "Jeu.php";

$jeu = new Jeu();
$jeu->chargerPartie();
$message = "";

if (isset($_POST['action']) && $_POST['action'] == "creer") {
$nom = $_POST['nom'];
$type = $_POST['type'];
$vie = intval($_POST['vie']);
$dps = intval($_POST['dps']);
$message = "Reçu : " . $nom . " (" . $type . "), " . $vie . " PV, " . $dps . " DPS";
}
?>

Afficher $message dans la page. Tester le formulaire et vérifier que les valeurs s'affichent correctement.


Bonne pratique - Tooujours nettoyer les données en entrée

Il est conseillé d'appliquer htmlspecialchars() sur toute chaîne saisie par l'utilisateur avant de l'afficher dans la page. Cette précaution élimine les risques de failles XSS (Cross-Site Scripting), qui permettraient à un utilisateur malveillant d'injecter du code JavaScript dans la page affichée aux autres visiteurs.

Étape 5 - Ajouter creerPersonnage à la classe Jeu

Ajouter dans Jeu une méthode publique creerPersonnage($nom, $type, $vie, $dps).

Elle doit :

  1. Déterminer automatiquement la position de départ du personnage selon son rang : le premier personnage créé part en (0, 0) (coin supérieur gauche), le deuxième en (4, 4) (coin inférieur droit, à l'opposé). Cette logique est encapsulée dans la méthode — index.php n'a pas à connaître les coordonnées de départ.
  2. Créer l'objet approprié selon $type : un Guerrier (avec armure 5, stockée dans $mana) ou un Mage (avec mana 100).
  3. Sauvegarder le personnage en base via $this->database->sauvegarderPersonnage($p), récupérer l'identifiant retourné et l'attribuer au personnage avec $p->setId(...).
  4. Ajouter le personnage dans $this->personnages.

Modifier index.php pour appeler cette méthode lorsque le formulaire est soumis, puis recharger la liste des personnages depuis la base.

Tester : créer deux personnages, vérifier que le premier apparaît en (0, 0) et le second en (4, 4), puis recharger la page et vérifier qu'ils persistent.


Bonne pratique - Toujours recharger les données après une modification

Après une action qui modifie la base (création, mise à jour, suppression), Il est conseillé de toujours recharger les données depuis la base avant d'afficher la page. Cela garantit que ce qui est affiché reflète exactement ce qui est en base, sans décalage entre les objets en mémoire et les données persistées.

Étape 6 - Ajouter le formulaire de déplacement

Le plateau de jeu est maintenant alimenté par la base. On veut permettre au joueur de déplacer un personnage d'une case dans une des quatre directions.

Au lieu d'une liste déroulante pour choisir la direction, on utilisera quatre boutons directionnels disposés en croix, placés à droite du plateau de jeu. Cette disposition est à la fois plus intuitive et plus proche de ce qu'un joueur attend d'un jeu.

La mise en page côte à côte (plateau à gauche, contrôles à droite) s'obtient avec du CSS flexbox sur un conteneur englobant.

La solution la plus propre sans JavaScript consiste à placer le sélecteur de combattant et les quatre boutons dans un seul et même formulaire. Chaque bouton est un <button type="submit" name="direction" value="..."> : quand on clique dessus, HTML soumet le formulaire en incluant la valeur de ce bouton dans $_POST['direction'], ainsi que la valeur courante du <select name="personnage"> visible. Le sélecteur et les boutons partagent ainsi le même formulaire, ce qui garantit que le combattant sélectionné est bien celui qui est déplacé.

Ajouter la méthode deplacerPersonnage($index, $direction) dans la classe Jeu. Elle doit :

  1. Vérifier que le personnage existe dans $this->personnages[$index] avec isset().
  2. Sauvegarder les coordonnées actuelles avant le déplacement.
  3. Appeler $p->deplacer($direction).
  4. Vérifier que le personnage ne sort pas du plateau (coordonnées entre 0 et $taillePlateau - 1). Si c'est le cas, annuler le déplacement en restaurant les coordonnées sauvegardées.
  5. Mettre à jour le personnage en base avec $this->database->mettreAJourPersonnage(...).

Bonne pratique - Ne pas redemander une information déjà connue du serveur

Quand une information peut être déduite du contexte côté serveur (ici, le joueur possède un seul personnage identifiable par sa session), ne pas la redemander à l'utilisateur. Supprimer un champ superflu simplifie l'interface et élimine une surface d'attaque : un joueur malveillant ne peut plus envoyer l'index d'un personnage qui ne lui appartient pas.

Étape 7 - Afficher le plateau avec les personnages chargés depuis la base

On va maintenant reconnecter le plateau de jeu (grille HTML) avec les personnages chargés depuis la base via la classe Jeu.

Le code de la grille est identique à celui de la séance 3, mais $personnages est maintenant fourni par $jeu->getPersonnages() et la taille du plateau par $jeu->getTaillePlateau().

Intégrer la grille dans index.php (après le traitement des formulaires, dans la partie HTML). Vérifier que :

  • les personnages créés s'affichent à leur position ;
  • après un déplacement, le personnage apparaît bien à sa nouvelle case ;
  • le plateau est vide si aucun personnage n'est en base.

Étape 8 - Ajouter le formulaire d'attaque

Ajouter dans la classe Jeu une méthode attaquer($indexAttaquant, $indexCible).

Elle doit :

  1. Vérifier que les deux index existent dans $this->personnages avec isset().
  2. Vérifier que l'attaquant et la cible ne sont pas le même personnage (un personnage ne peut pas s'attaquer lui-même).
  3. Vérifier que la cible est encore en vie (points de vie supérieurs à 0).
  4. Appeler $attaquant->attaquer($cible).
  5. Mettre à jour la cible en base.

Ajouter dans la partie HTML de index.php le formulaire d'attaque, visible uniquement s'il y a au moins deux personnages. Chaque liste déroulante (attaquant et cible) affiche les personnages vivants uniquement.


Bonne pratique - Séparer la logique métier de la présentation

Construire la liste des vivants dans index.php (côté affichage) plutôt que dans Jeu. C'est un choix de présentation (n'afficher que les vivants dans le formulaire), pas une règle du jeu. La distinction entre logique métier (dans les classes) et logique de présentation (dans index.php) doit toujours être respectée.

Étape 9 - Afficher un message de retour après chaque action

Pour l'instant, après une action, la page se recharge en silence. L'utilisateur ne sait pas si son action a fonctionné. Il faut afficher un message clair.

La variable $message est déjà initialisée à "" en haut du fichier. Elle est remplie dans chaque bloc de traitement. Il reste à l'afficher dans la page HTML de manière visible.

Ajouter dans la partie HTML, juste après la balise <body>, un bloc conditionnel qui affiche le message dans une <div> stylisée, uniquement si $message n'est pas vide.

Utiliser la syntaxe alternative if(): / endif;.

Affiner également les messages pour qu'ils soient plus informatifs :

  • pour la création : indiquer le nom et le type du personnage créé ;
  • pour le déplacement : indiquer le nom du personnage et sa nouvelle position ;
  • pour l'attaque : indiquer le nom de l'attaquant, le nom de la cible et les PV restants.

Pour les deux derniers cas, il faut lire l'état du jeu après l'action (appeler chargerPartie() avant de construire le message).


Bonne pratique - Toujours informer l'utilisateur du résultat de ses actions

Il est conseillé de toujours informer l'utilisateur du résultat de ses actions. Une interface qui ne donne aucun retour force l'utilisateur à inspecter la page pour comprendre ce qui s'est passé. Un message simple et précis améliore considérablement l'expérience.

Étape 10 - Ajouter un bouton de réinitialisation de la partie

Pour pouvoir recommencer une partie, ajouter un formulaire simple avec un seul bouton :

<h2>Gestion de la partie</h2>
<form method="POST" action="index.php">
<input type="hidden" name="action" value="reinitialiser" />
<input type="submit" value="Nouvelle partie" />
</form>

Ajouter dans la classe Jeu une méthode reinitialiser() qui appelle $this->database->supprimerTousLesPersonnages() et réinitialise $this->personnages à un tableau vide.

Traiter cette action dans index.php.

Une précaution importante : ce bouton supprime toutes les données. Ajouter un attribut onclick qui demande une confirmation à l'utilisateur avant d'envoyer le formulaire :

<input type="submit" value="Nouvelle partie"
onclick="return confirm('Supprimer tous les personnages et recommencer ?');" />

Bonne pratique - Demander confirmation pour les actions destructrices

Attention, toute action destructrice (suppression, réinitialisation) doit demander confirmation avant d'être exécutée. Même dans un prototype de jeu, cette règle préserve du clics accidentels. Dans une application de production, on irait plus loin avec une double confirmation ou une page dédiée.


Solution complète

Solution complète du TP

Exercices complémentaires

Exercice A - Afficher l'identifiant de chaque personnage dans le plateau

Modifier l'affichage des cellules de la grille pour y faire apparaître l'identifiant SQLite de chaque personnage (son id). Cela sera utile pour déboguer les problèmes de mise à jour.

Vérifier dans DB Browser for SQLite que les identifiants correspondent bien à ceux stockés en base.

Exercice B - Permettre la création d'un troisième personnage ou plus

La méthode creerPersonnage() place le 1er personnage en (0, 0) et le 2ème en (4, 4). Si l'on crée un 3ème personnage (ou davantage), la règle actuelle le placerait aussi en (4, 4), ce qui provoquerait une superposition.

Modifier creerPersonnage() pour que tout personnage au-delà du deuxième soit placé sur la première case libre trouvée en parcourant le plateau de gauche à droite, ligne par ligne.

Exercice C - Ajouter la validation côté serveur sur la création

Pour l'instant, la validation de la création repose uniquement sur les attributs HTML (required, min). Un utilisateur peut contourner ces contrôles en modifiant le HTML dans l'inspecteur du navigateur ou en envoyant une requête HTTP directement.

Ajouter une validation côté PHP avant d'appeler creerPersonnage() :

  • Le nom ne doit pas être vide (après trim()).
  • Les points de vie doivent être supérieurs à 0.
  • Les dégâts doivent être supérieurs à 0.
  • Le type doit être "guerrier" ou "mage".

En cas d'erreur, mettre un message d'erreur dans $message et ne pas créer le personnage.


Ce qu'il faut retenir

NotionRésumé
Méthode POSTLes données de formulaire sont envoyées dans le corps de la requête, invisibles dans l'URL.
$_POSTTableau superglobal contenant les données envoyées par un formulaire POST.
isset()Vérifie qu'une variable existe et n'est pas null avant d'y accéder.
intval()Convertit une chaîne en entier. Toutes les valeurs de $_POST sont des chaînes.
htmlspecialchars()Neutralise les balises HTML dans les données utilisateur. Protège contre les failles XSS.
trim()Supprime les espaces superflus en début et fin de chaîne.
Champ hiddenChamp invisible dans le formulaire, utilisé pour transmettre l'identifiant de l'action.
Syntaxe alternativeif(): / endif;, foreach(): / endforeach; - plus lisible dans les templates HTML.
Classe JeuCentralise la logique métier. index.php délègue les actions à Jeu, qui délègue la persistance à Database.
Séparation des responsabilitésLogique métier dans les classes, présentation dans index.php.

Aperçu de la prochaine séance

Le jeu est désormais interactif : on peut créer des personnages, les déplacer et les faire combattre. Il manque encore la détection de fin de partie et quelques améliorations d'expérience de jeu.

La séance 6 finalisera l'application : système de tours, victoire quand un seul personnage reste en vie, historique des actions de la partie, et amélioration de l'affichage global.