2) Organiser son code
Organiser son code avec des classes

Objectifs de la séance
- Comprendre le problème que les classes viennent résoudre.
- Créer une classe avec des propriétés et des méthodes.
- Contrôler l'accès aux données avec les mots-clés
privateetpublic. - Utiliser les accesseurs (
get/set) pour lire et modifier des propriétés de manière sécurisée. - Créer une classe fille qui hérite d'une classe parente avec
extends. - Déclarer une classe abstraite qui ne peut pas être instanciée directement.
Notions théoriques
Le problème des variables en vrac
À la fin de la séance 1, le code de index.php ressemblait à ceci :
$nom1 = "Lara";
$vie1 = 100;
$dps1 = 10;
$nom2 = "Mario";
$vie2 = 80;
$dps2 = 15;
Trois variables par personnage. Si l'on ajoute un troisième attribut demain (par exemple une armure),
il faut ajouter $armure1, $armure2, $armure3... Pour passer un personnage à une fonction,
on est obligé de transmettre toutes ses variables une par une :
simulerCombat($nom1, $vie1, $dps1, $nom2, $vie2, $dps2);
Six paramètres pour deux personnages. Avec une armure, ce serait huit. Ce code est difficile à lire et encore plus difficile à maintenir.
Les classes permettent de regrouper les données et les actions d'une entité dans un seul endroit.
Qu'est-ce qu'une classe ?
Une classe est un modèle. Elle décrit la structure d'un objet : quelles données il contient (ses propriétés) et quelles actions il peut effectuer (ses méthodes).
Un objet est une instance de la classe : c'est un exemplaire concret créé à partir du modèle.
L'analogie classique : une classe est comme un plan d'architecte, et un objet est la maison construite à partir de ce plan. On peut construire plusieurs maisons (objets) à partir du même plan (classe), chacune avec ses propres caractéristiques (couleur, surface...).
// Définir la classe (le modèle)
class Personnage {
// propriétés et méthodes...
}
// Créer des objets (des instances)
$p1 = new Personnage();
$p2 = new Personnage();
Propriétés et méthodes
Les propriétés sont les variables internes à la classe. Elles décrivent l'état de l'objet.
Les méthodes sont les fonctions internes à la classe. Elles décrivent le comportement de l'objet.
À l'intérieur d'une méthode, on accède aux propriétés et aux autres méthodes de la même classe
via $this (qui signifie "cet objet-ci", celui sur lequel on est en train de travailler) :
class Personnage {
public $nom;
public $vie;
public function sePresenter() {
print "<p>Je suis " . $this->nom . " et j'ai " . $this->vie . " points de vie.</p>";
}
}
$p = new Personnage();
$p->nom = "Lara";
$p->vie = 100;
$p->sePresenter(); // affiche : Je suis Lara et j'ai 100 points de vie.
Le constructeur
La méthode spéciale __construct est appelée automatiquement au moment où l'on crée un objet
avec new. Elle sert à initialiser les propriétés dès la création, plutôt que de les remplir
une par une ensuite.
class Personnage {
public $nom;
public $vie;
public function __construct($nom, $vie) {
$this->nom = $nom;
$this->vie = $vie;
}
}
$p = new Personnage("Lara", 100);
// $p->nom vaut "Lara", $p->vie vaut 100
Contrôler l'accès : private et public
Le mot-clé public signifie que la propriété ou la méthode est accessible depuis n'importe où
(depuis l'extérieur de la classe, depuis une autre classe...).
Le mot-clé private signifie que la propriété ou la méthode n'est accessible que depuis
l'intérieur de la classe elle-même.
Pourquoi utiliser private ? Pour se protéger des erreurs. Si $vie est publique, n'importe
quel code peut écrire $p->vie = -999; sans aucune vérification. Si elle est privée, on est
obligé de passer par une méthode setVie() qui peut vérifier que la valeur est cohérente
avant de l'enregistrer.
class Personnage {
private $vie;
public function setVie($vie) {
if (!is_numeric($vie)) {
print "La vie doit être un nombre";
return;
}
$this->vie = $vie;
}
public function getVie() {
return $this->vie;
}
}
Ces méthodes de lecture et de modification s'appellent des accesseurs :
getVie()est un "getter" (lire la propriété) ;setVie($vie)est un "setter" (modifier la propriété, avec validation).
La méthode __toString
PHP permet de définir ce qui doit être affiché quand on utilise print directement sur un objet.
C'est le rôle de la méthode spéciale __toString : elle doit renvoyer une chaîne de caractères.
public function __toString() {
return "Le personnage " . $this->nom . " a " . $this->vie . " points de vie";
}
$p = new Personnage("Lara", 100, 10);
print $p; // affiche : Le personnage Lara a 100 points de vie
L'héritage
L'héritage permet de créer une classe plus spécialisée à partir d'une classe existante. La classe fille récupère toutes les propriétés et méthodes de la classe parente, et peut en ajouter de nouvelles ou en redéfinir certaines.
On utilise le mot-clé extends :
class Guerrier extends Personnage {
public function __construct($nom, $vie, $dps, $mana) {
parent::__construct($nom, $vie, $dps, $mana); // transmet $mana au parent
}
public function attaquer($cible) {
// chaque sous-classe implémente sa propre logique d'attaque
}
}
parent::__construct(...) appelle le constructeur de la classe parente. C'est obligatoire
quand on redéfinit le constructeur dans une classe fille.
Une méthode de la classe fille peut aussi redéfinir (remplacer) une méthode du parent. C'est ce qu'on appelle la surcharge (override). La méthode de la classe fille prend le dessus sur celle du parent.
Les classes abstraites
Le mot-clé abstract devant une classe signifie qu'on ne peut pas créer d'objet directement
à partir d'elle. On doit obligatoirement passer par une classe fille.
C'est utile quand la classe parente est trop générique pour exister seule. Dans notre jeu,
il n'y a pas de "personnage générique" : il y a des guerriers, des mages, etc.
La classe Personnage n'existe que comme modèle commun ; c'est pourquoi elle est abstraite.
abstract class Personnage {
// ...
}
$p = new Personnage(...); // Erreur : impossible d'instancier une classe abstraite
$g = new Guerrier(...); // Correct : Guerrier est une classe fille concrète
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Ce TP reprend le projet jeu-de-combat créé lors de la séance 1.
On repart du code final de cette séance, qui contient 2 fichiers :
index.phpfonctions.php.
À la fin de cette séance, le projet contiendra 2 fichiers PHP supplémentaires :
Personnage.php,Guerrier.php.
Et le fichier index.php sera réécrit pour utiliser les classes à la place des fonctions et des variables en vrac.
Le fichier fonctions.php sera conservé mais vidé de son contenu utile, au fur et à mesure
que les fonctions seront remplacées par des méthodes de classes.
$dps et $manaLa classe Personnage portera deux propriétés communes à tous les types de personnages :
$dps(damage per second) : la puissance d'attaque. Pour un guerrier, c'est la force de frappe ; pour un mage, c'est la puissance de son sort.$mana: la ressource défensive. Pour un guerrier, c'est l'épaisseur de son bouclier ; pour un mage, c'est sa réserve d'énergie magique.
Ces noms viennent du vocabulaire des jeux de rôle numériques, où dps désigne les dégâts infligés par seconde et mana l'énergie magique. Ils sont courts, expressifs, et compris immédiatement par tout développeur qui a déjà touché à un jeu vidéo. Une alternative plus générique comme $puissance et $protection serait correcte sur le plan sémantique, mais moins parlante dans ce contexte ludique.
Étape 1 - Créer la classe Personnage avec des propriétés publiques
On commence par la version la plus simple possible d'une classe, sans protection ni constructeur, pour comprendre la syntaxe de base.
Créer un fichier Personnage.php dans le dossier du projet.
Y déclarer une classe Personnage avec trois propriétés publiques : $nom, $vie et $dps.
Ajouter une méthode publique sePresenter() qui affiche une ligne de texte HTML contenant
le nom et les points de vie du personnage.
Dans index.php, inclure Personnage.php avec require_once, créer deux objets Personnage,
affecter leurs propriétés, puis appeler sePresenter() sur chacun.
L'affichage attendu :
Jeu de combat PHP
Le personnage Lara a 100 points de vie.
Le personnage Mario a 80 points de vie.
Une classe = un fichier. Le nom du fichier doit correspondre exactement
au nom de la classe, avec la même casse (Personnage.php pour la classe Personnage).
Cette convention est universelle dans l'écosystème PHP. Elle rend la navigation dans
le projet immédiate.
Étape 2 - Ajouter un constructeur
Affecter les propriétés une par une après new Personnage() est fastidieux.
Le constructeur permet de tout initialiser dès la création de l'objet.
Ajouter un constructeur __construct($nom, $vie, $dps) à la classe Personnage.
Le constructeur doit simplement affecter chaque paramètre à la propriété correspondante
via $this.
Modifier index.php pour créer les deux personnages en passant les valeurs directement
à new Personnage(...), sans affecter les propriétés une par une.
Le constructeur doit se limiter à l'initialisation des propriétés. Il ne doit pas effectuer de calculs complexes ni appeler de fonctions extérieures susceptibles d'échouer. Un constructeur qui lève une exception est difficile à gérer pour le code appelant.
Étape 3 - Passer les propriétés en private et ajouter des accesseurs
Les propriétés publiques sont un problème : n'importe quel code peut écrire
$p1->vie = -500; sans qu'aucune vérification ne soit effectuée.
Passer les trois propriétés de Personnage en private.
Ajouter les six méthodes accesseurs suivantes :
getNom(): renvoie$this->nom.setNom($nom): vérifie que$nomest bien une chaîne de caractères avecis_string(), affiche un message d'erreur si ce n'est pas le cas, sinon affecte la valeur.getVie(): renvoie$this->vie.setVie($vie): vérifie que$vieest bien un nombre avecis_numeric(), affiche un message d'erreur sinon, puis affecte la valeur.getDps(): renvoie$this->dps.setDps($dps): affecte simplement la valeur (pas de validation particulière pour le moment).
Modifier le constructeur pour qu'il appelle setNom, setVie et setDps au lieu d'affecter
directement les propriétés. Ainsi, la validation est automatiquement effectuée à la création.
Modifier la méthode sePresenter() pour qu'elle utilise les getters (getNom(), getVie())
plutôt que d'accéder directement à $this->nom et $this->vie.
Dans index.php, remplacer tout accès direct aux propriétés par des appels aux accesseurs.
Les propriétés sont toujours déclarées en private. On n'accède jamais
directement à une propriété depuis l'extérieur de la classe. On passe systématiquement
par les accesseurs. Cette discipline garantit que les données d'un objet sont toujours
dans un état cohérent, quelle que soit la manière dont on les manipule.
Étape 4 - Ajouter la méthode __toString et la méthode attaquer
Deux méthodes manquent encore à la classe Personnage.
Ajouter la méthode __toString(). Elle doit renvoyer (avec return, pas avec print)
la chaîne suivante : "Le personnage [nom] a [vie] points de vie".
Grâce à cette méthode, on pourra écrire print $p1; directement, sans appeler
sePresenter(). PHP appellera automatiquement __toString() quand on utilise
print sur un objet.
Ajouter ensuite la méthode attaquer($cible). Elle prend en paramètre un autre objet
Personnage (la cible). Elle doit réduire les points de vie de la cible du montant
des DPS de l'attaquant, en utilisant les accesseurs : $cible->setVie($cible->getVie() - $this->getDps()).
Dans index.php, remplacer sePresenter() par print $p1; et print $p2;
(avec un <br/> entre les deux). Faire attaquer $p1 sur $p2, puis afficher
les deux personnages après l'attaque pour vérifier que les points de vie ont bien changé.
__toString__toString doit renvoyer une chaîne de caractères avec return,
et jamais l'afficher avec print. Si la méthode affiche directement, elle ne peut
plus être utilisée dans des contextes où l'on veut récupérer la représentation textuelle
sans l'afficher (pour la mettre dans une variable, dans un fichier de log, etc.).
Étape 5 - Rendre la classe abstraite
La classe Personnage est trop générique pour exister seule. Dans ce jeu, on ne crée
jamais un "personnage" au sens abstrait : on crée un guerrier, un mage, un archer...
Autrement dit, Personnage ne sert que de modèle commun ; ce n'est pas un type qu'on
instancie directement.
Effectuer les trois modifications suivantes sur la classe Personnage :
1. Ajouter le mot-clé abstract devant la déclaration de la classe.
2. Ajouter la propriété $mana : un entier représentant la ressource défensive du personnage
(bouclier pour un guerrier, mana pour un mage). Ajouter le paramètre $mana au
constructeur (après $dps), et les accesseurs setMana($mana) et getMana().
3. Remplacer la méthode concrète attaquer($cible) par une déclaration abstraite :
abstract public function attaquer($cible);
Chaque type de personnage attaque différemment : la classe parente déclare que "tout personnage peut attaquer", mais laisse le COMMENT aux classes filles. C'est une méthode abstraite.
Dans index.php, essayer de créer un new Personnage(...) et observer le message d'erreur.
Comprendre l'erreur, puis supprimer cette ligne.
À ce stade, index.php ne peut plus créer de personnages : Personnage est abstraite et
exige que ses classes filles implémentent attaquer(). La prochaine étape crée Guerrier.
Il est conseillé de déclarer une classe abstraite dès qu'elle ne doit pas être instanciée
directement. C'est une contrainte volontaire qui documente l'intention du développeur
et empêche les erreurs d'utilisation. Si quelqu'un essaie de créer un Personnage
par erreur, PHP l'arrêtera immédiatement avec un message clair.
Étape 6 - Créer la classe Guerrier par héritage
Créer un nouveau fichier Guerrier.php.
La classe Guerrier hérite de Personnage (avec extends). La propriété $mana
est déjà dans Personnage : le guerrier l'utilise comme bouclier sans avoir besoin de la redéfinir.
Écrire le constructeur __construct($nom, $vie, $dps, $mana). Il doit uniquement
transmettre tous les paramètres au constructeur parent :
parent::__construct($nom, $vie, $dps, $mana).
La classe Personnage déclare attaquer() comme abstraite : toute classe fille concrète
doit l'implémenter. Pour l'instant, écrire une version minimale (les dégâts n'utilisent
pas encore $mana) — la logique complète viendra à l'étape suivante.
Dans index.php, remplacer les new Personnage(...) par des new Guerrier(...).
Ajouter require_once "Guerrier.php"; en haut du fichier.
Vérifier que l'affichage fonctionne toujours : le guerrier hérite de __toString
et peut donc être affiché avec print.
Dans le constructeur de la classe fille, appeler parent::__construct(...)
en tout premier. Cela garantit que les propriétés de la classe parente sont initialisées
(et validées) avant que la classe fille n'ajoute les siennes. Oublier cet appel est
une source fréquente de bugs où les propriétés héritées restent non initialisées.
Étape 7 - Améliorer la méthode attaquer pour tenir compte du mana
Pour le moment, la méthode attaquer du guerrier réduit simplement la vie de la cible
de la valeur de $dps, sans tenir compte du mana de la cible.
On veut que les dégâts soient réduits par le mana de la cible. Si la cible a une mana de 5 et que l'attaquant inflige 10 DPS, les dégâts effectifs sont de 5. Si le mana est supérieur aux DPS, les dégâts tombent à 0.
Modifier la méthode attaquer($cible) dans la classe Guerrier. Elle doit :
- Récupérer les DPS de l'attaquant avec
$this->getDps(). - Si le mana de la cible est supérieur à 0, soustraire sa valeur aux DPS :
$cible->getMana()fonctionne sur n'importe quelPersonnage— pas besoin deinstanceof! - S'assurer que les DPS ne descendent pas en dessous de 0.
- Appliquer les dégâts effectifs à la cible avec
$cible->setVie(...).
Tester dans index.php avec deux guerriers dont l'un a un mana plus élevé que les DPS
de l'autre, pour vérifier que les dégâts tombent bien à 0 dans ce cas.
Il est conseillé de toujours penser aux cas limites lors de l'écriture d'une méthode. Ici, le cas où le mana dépasse les DPS (résultat négatif) doit être traité explicitement. Un personnage ne doit jamais gagner de la vie parce qu'il s'est fait attaquer. Tester systématiquement ces cas extrêmes est une habitude qui évite beaucoup de bugs.
Étape 8 - Simuler un combat complet avec les objets
La séance 1 utilisait une fonction simulerCombat qui prenait six paramètres.
On va maintenant réécrire cette logique en utilisant les objets.
Dans index.php, écrire une boucle while qui simule un combat complet entre deux guerriers.
À chaque tour, le premier attaque le deuxième, puis le deuxième attaque le premier
(si ce dernier est encore en vie). La boucle s'arrête quand l'un des deux n'a plus
de points de vie.
On ne crée pas de nouvelle fonction ni de nouvelle méthode : la logique de la boucle
est écrite directement dans index.php. L'objectif est de constater à quel point
le code est plus lisible avec des objets qu'avec des variables en vrac.
Afficher un titre "Tour N" à chaque itération, puis l'état des deux personnages après les attaques.
Comparer le code de cette étape avec la fonction simulerCombat
de la séance 1. Les 6 paramètres ($nom1, $vie1, $dps1, $nom2, $vie2, $dps2)
ont disparu. Le code est plus court, plus lisible, et il n'y a plus besoin de retourner
un tableau pour récupérer les données du vainqueur : l'objet les porte avec lui.
Étape 9 - Expérimenter les erreurs pour comprendre les protections
Cette étape est volontairement différente des précédentes : il s'agit de provoquer des erreurs pour comprendre ce que les mécanismes mis en place protègent réellement.
Réaliser les quatre expériences suivantes dans index.php. Pour chacune, noter le message
d'erreur affiché par PHP, comprendre pourquoi il apparaît, puis supprimer la ligne fautive
avant de passer à la suivante.
Expérience A - Tenter d'instancier une classe abstraite :
$p = new Personnage("Lara", 100, 10);
Expérience B - Tenter d'accéder directement à une propriété privée :
$p1 = new Guerrier("Lara", 100, 10, 5);
print $p1->nom; // propriété privée
Expérience C - Passer une valeur invalide à un setter :
$p1 = new Guerrier("Lara", 100, 10, 5);
$p1->setVie("beaucoup");
print $p1; // que vaut la vie de Lara ?
Expérience D - Passer un nombre à setNom :
$p1 = new Guerrier("Lara", 100, 10, 5);
$p1->setNom(42);
print $p1;
Provoquer volontairement des erreurs est une bonne manière de comprendre les mécanismes de protection du code. Un développeur qui ne sait pas ce que provoque une erreur ne peut pas anticiper les comportements inattendus de son programme.
Étape 10 - Finaliser le code et vérifier la cohérence avec l'état attendu
Il est temps de vérifier que le code produit correspond exactement à l'état attendu décrit en début de TP (voir la section "Point de départ du TP").
Comparer ligne par ligne Personnage.php, Guerrier.php et index.php avec le code cible.
Quelques points à vérifier :
Personnageestabstract, a la propriété$manaavec ses accesseurs, et déclareattaquer()comme abstraite (pas d'implémentation dans le parent).Guerrierimplémenteattaquer()en utilisant$cible->getMana()— accessible sur n'importe quelPersonnage, sans besoin deinstanceof.- Le
index.phpcible utilise les personnages "Lara" et "Miss Marple". Adapter le fichier en conséquence.
Une fois ces vérifications faites, s'assurer que le code fonctionne en lançant le serveur
et en chargeant http://localhost:8000 dans le navigateur.
Solution complète
Solution complète du TP
Vous devez être connecté pour voir le contenu.
Exercices complémentaires
Exercice A - Ajouter un affichage du mana dans __toString
Modifier la méthode __toString de la classe Guerrier pour qu'elle renvoie une chaîne
plus complète, incluant la valeur du mana. Par exemple :
Le guerrier Lara a 100 points de vie et 5 de mana
Observer qu'on surcharge ici __toString dans la classe fille, tout comme on a surchargé
attaquer. La méthode de la classe fille prend le dessus sur celle du parent.
Exercice B - Simuler un tournoi à trois guerriers
Créer trois guerriers avec des caractéristiques différentes. Organiser deux combats :
- Le premier guerrier contre le deuxième.
- Le vainqueur contre le troisième.
La difficulté : après le premier combat, le vainqueur a perdu des points de vie. Il entre dans le second combat affaibli. C'est un avantage de l'approche par objets : l'objet "porte" son état avec lui, sans qu'on ait besoin de le stocker et de le transmettre manuellement.
Exercice C - Comprendre la visibilité private entre instances
Voici un code à analyser. Avant de l'exécuter, essayer de prédire ce qu'il fait et si PHP affiche une erreur :
<?php
require_once "Personnage.php";
require_once "Guerrier.php";
$p1 = new Guerrier("Lara", 100, 10, 5);
$p2 = new Guerrier("Miss Marple", 100, 15, 7);
// Accès à la propriété privée $vie de $p2 depuis une méthode de Personnage
// (c'est ce que fait attaquer() dans le code d'origine du plan du cours)
// $p2->vie = $p2->vie - 10; // ligne commentée : ferait une erreur depuis l'extérieur
// Mais depuis l'intérieur de la classe, PHP autorise l'accès aux propriétés privées
// d'une autre instance de la même classe.
Pour vérifier, ajouter temporairement dans Personnage.php une méthode :
public function copierVie($autrePersonnage) {
// Accès direct à la propriété privée d'une autre instance de la même classe
$this->vie = $autrePersonnage->vie;
}
Appeler $p1->copierVie($p2) dans index.php et vérifier que cela fonctionne sans erreur.
Puis essayer d'écrire $p2->vie = 50; directement dans index.php et observer la différence.
Une solution complète
Vous devez être connecté pour voir le contenu.
Ce qu'il faut retenir
| Notion | Résumé |
|---|---|
| Classe | Modèle décrivant les propriétés et les méthodes d'un type d'objet. |
| Objet | Instance d'une classe, créée avec new. |
$this | Référence à l'objet courant, dans une méthode de la classe. |
__construct | Méthode appelée automatiquement à la création de l'objet. |
private | Propriété ou méthode accessible uniquement depuis l'intérieur de la classe. |
public | Propriété ou méthode accessible depuis n'importe où. |
| Accesseurs | Méthodes get et set pour lire et modifier des propriétés privées avec validation. |
__toString | Méthode appelée automatiquement quand on utilise print sur un objet. |
abstract | Classe qui ne peut pas être instanciée directement. |
extends | Mot-clé pour faire hériter une classe d'une autre. |
parent::__construct | Appel du constructeur de la classe parente depuis la classe fille. |
| Surcharge | Redéfinition d'une méthode héritée dans la classe fille. |
Aperçu de la prochaine séance
Le jeu ne comporte pour le moment qu'un seul type de personnage : le guerrier. La séance 3 ajoutera un second type, le mage, qui a une mécanique de jeu différente : il possède une réserve de mana et attaque en lançant des sorts offensifs.
On ajoutera aussi les coordonnées $x et $y à la classe Personnage, ce qui permettra
de positionner les personnages sur un plateau de jeu et de les déplacer.
Ce plateau sera affiché sous forme de grille HTML avec un tableau <table>.