Objets et classes
Séance 1 · ~2 périodes
À la fin de ce chapitre, vous serez capables de :
- définir un objet comme la réunion d'un état et d'un comportement ;
- écrire une classe PHP moderne : propriétés typées, constructeur avec promotion,
readonly,declare(strict_types=1); - justifier pourquoi une classe expose
takeDamage()plutôt quesetHp(); - distinguer une méthode d'instance d'une méthode statique et écrire une fabrique
Dice::d6().
1. Le problème : un héros en tableau associatif
On veut un héros de jeu de rôle : un nom, des points de vie, une force. Il encaisse des coups, il se soigne, il peut mourir. Avec ce que vous savez déjà, cela donne un tableau et trois fonctions.
<?php
declare(strict_types=1);
$hero = ['name' => 'Arthur', 'hp' => 10, 'maxHp' => 10, 'strength' => 2];
function takeDamage(array $hero, int $amount): array
{
$hero['hp'] = max(0, $hero['hp'] - $amount);
return $hero;
}
function heal(array $hero, int $amount): array
{
$hero['hp'] = min($hero['maxHp'], $hero['hp'] + $amount);
return $hero;
}
function describe(array $hero): string
{
return "{$hero['name']} ({$hero['hp']}/{$hero['maxHp']} PV)";
}
$hero = takeDamage($hero, 3);
$hero = heal($hero, 1);
echo describe($hero), PHP_EOL;
$hero['hp'] = 9999;
$hero['maxHP'] = 12;
echo describe($hero), PHP_EOL;
Arthur (8/10 PV)
Arthur (9999/10 PV)
Le résultat est correct. Ce code a pourtant quatre défauts. Ce sont les quatre que la POO corrige.
- Les données et les fonctions qui les manipulent sont séparées. Rien n'indique que
heal()concerne le héros. Avec quarante fonctions dans le fichier, plus personne ne sait ce qui va avec quoi. - Il faut réassigner à chaque appel :
$hero = takeDamage($hero, 3);. Si vous oubliez$hero =, les points de vie ne changent pas et rien ne le signale. - Rien n'est protégé. La ligne
$hero['hp'] = 9999;passe. Les règles du jeu sont : jamais sous 0, jamais au-dessus demaxHp. Elles ne tiennent que si tout le monde passe par ces trois fonctions. - Rien n'est vérifié. Une affectation à
$hero['maxHP'], avec unHmajuscule, ajoute une clé au tableau au lieu de modifiermaxHp. Le bug apparaîtra plus tard, ailleurs.
2. La même chose, en objet
Deux mots avant de lire le code qui suit. Une propriété est une variable qui appartient à l'objet : elle remplace ici une case du tableau. Une méthode est une fonction qui appartient à l'objet : elle remplace ici une des trois fonctions. Le reste n'est que de la syntaxe.
<?php
declare(strict_types=1);
class Hero
{
private int $hp;
private int $maxHp;
public function __construct(
public readonly string $name,
int $maxHp = 10,
public readonly int $strength = 2,
) {
$this->maxHp = $maxHp;
$this->hp = $maxHp;
}
public function hp(): int
{
return $this->hp;
}
public function maxHp(): int
{
return $this->maxHp;
}
public function takeDamage(int $amount): void
{
$this->hp = max(0, $this->hp - $amount);
}
public function heal(int $amount): void
{
$this->hp = min($this->maxHp, $this->hp + $amount);
}
public function isAlive(): bool
{
return $this->hp > 0;
}
public function __toString(): string
{
return sprintf('%s (%d/%d PV)', $this->name, $this->hp, $this->maxHp);
}
}

On s'en sert comme ceci :
$hero = new Hero('Arthur');
$hero->takeDamage(3);
$hero->heal(1);
echo $hero, PHP_EOL;
$hero->takeDamage(50);
echo $hero, PHP_EOL;
echo $hero->isAlive() ? 'vivant' : 'mort', PHP_EOL;
Arthur (8/10 PV)
Arthur (0/10 PV)
mort
Même résultat qu'avec le tableau. Mais $hero est maintenant un seul bloc : ses données et les méthodes qui les modifient sont au même endroit. On ne réassigne plus, car $hero->takeDamage(3) modifie le héros lui-même.
Note
Un objet réunit un état et un comportement. L'état, ce sont ses propriétés : name, hp, maxHp, strength. Le comportement, ce sont ses méthodes : takeDamage(), heal(), isAlive(). Tout le vocabulaire qui suit sert à décrire cette phrase.
3. Le vocabulaire
class : le moule, new : la pièce
class Hero { … } ne crée aucun héros. C'est un moule : la description des données et des méthodes que possède tout héros. new Hero('Arthur') fabrique une instance, c'est-à-dire un héros concret, avec ses propres valeurs, en mémoire.
Avec un seul moule, vous fabriquez autant d'objets que vous voulez. Chacun a son propre état.
$arthur = new Hero('Arthur');
$morgane = new Hero('Morgane', maxHp: 8, strength: 3);
$arthur->takeDamage(3);
echo $arthur, PHP_EOL;
echo $morgane, PHP_EOL;
Arthur (7/10 PV)
Morgane (8/8 PV)
Les points de vie de Morgane n'ont pas changé : chaque héros possède ses propres valeurs. Au passage, maxHp: 8 est un argument nommé. Il donne la valeur d'un paramètre par son nom, ce qui permet de sauter ceux qui ont une valeur par défaut.
$this : l'objet en cours
Dans une méthode, $this désigne l'objet sur lequel la méthode a été appelée. Quand vous écrivez $arthur->takeDamage(3), PHP exécute le corps de takeDamage avec $this qui vaut $arthur. C'est ce qui permet d'écrire la méthode une seule fois pour tous les héros du jeu.
Propriétés typées et declare(strict_types=1)
private int $hp; déclare une propriété qui n'accepte qu'un entier. La ligne declare(strict_types=1), en tête de chaque fichier, dit à PHP de refuser les conversions silencieuses.
declare(strict_types=1);
function takeDamage(int $amount): int
{
return $amount;
}
takeDamage('3');
Fatal error: Uncaught TypeError: takeDamage(): Argument #1 ($amount)
must be of type int, string given
Sans cette ligne, PHP accepterait '3' et le convertirait en 3 sans rien dire. Le typage strict fait apparaître l'erreur là où elle est, pas cent lignes plus loin.
Le constructeur, et la promotion
Le constructeur est la méthode appelée automatiquement par new. Il initialise les propriétés de l'objet. Avant PHP 8, il fallait écrire chaque propriété trois fois.
// L'ancienne façon, celle que vous croiserez dans du code plus vieux.
class Hero
{
private string $name;
public function __construct(string $name)
{
$this->name = $name;
}
}
Note
Histoire de PHP. Le langage s'est équipé par étapes. PHP 5.4, en 2012, ajoute la syntaxe courte des tableaux : [] remplace array(), que vous verrez encore dans du code ancien. PHP 7.0, en 2015, ajoute les types scalaires sur les paramètres et les retours, ainsi que declare(strict_types=1). PHP 7.4, en 2019, ajoute les types sur les propriétés. PHP 8.0, en 2020, ajoute la promotion de constructeur et les arguments nommés. PHP 8.1, en 2021, ajoute readonly. Un fichier sans types ni promotion n'est pas du mauvais code : il est plus ancien que ces versions.
La promotion de constructeur fait les trois opérations en une.
public function __construct(
public readonly string $name,
int $maxHp = 10,
public readonly int $strength = 2,
) {
$this->maxHp = $maxHp;
$this->hp = $maxHp;
}
Un paramètre est une valeur reçue par une méthode, le temps de son exécution. Une propriété, elle, reste dans l'objet. Un mot de visibilité devant un paramètre, comme public devant $name, crée la propriété correspondante et lui donne la valeur reçue. C'est le cas de name et de strength. Sans ce mot, $maxHp reste un simple paramètre, et le constructeur doit le recopier lui-même.
Le corps du constructeur sert à ce qui se calcule. Ici, hp reçoit la valeur de maxHp : le héros commence avec tous ses points de vie. C'est une règle du jeu, pas un paramètre. hp et maxHp sont l'affaire de la classe : on les lit par hp() et maxHp(), on ne les écrit jamais de l'extérieur.
readonly : ce qui ne change jamais
Un héros ne change pas de nom en cours de partie. readonly l'écrit dans le code : la propriété reçoit sa valeur une fois, dans le constructeur, et ne peut plus être modifiée ensuite.
Visibilité : public, private, protected
public : accessible de partout. private : accessible uniquement depuis l'intérieur de la classe. protected : comme private, mais aussi depuis les classes filles. On en reparle au chapitre 5, quand les classes filles existeront.
Encapsulation : pourquoi il n'y a pas de setHp()
C'est le point le plus important du chapitre. Voici deux écritures que PHP refuse maintenant.
$arthur->hp = 9999; // propriété privée
$arthur->name = 'Kevin'; // propriété readonly

Chaque essai doit être lancé seul, car la première erreur arrête le script. Voici les deux messages.
Fatal error: Uncaught Error: Cannot access private property Hero::$hp
Fatal error: Uncaught Error: Cannot modify readonly property Hero::$name
Le 9999 de la partie 1 est devenu impossible. La classe est le seul endroit où hp change : c'est cela, l'encapsulation.
Attention
Le réflexe scolaire est d'ajouter un getter et un setter pour chaque propriété. Ne le faites pas. Un setHp() sans vérification permettrait de redonner 9999 points de vie au héros.
// Mauvais : la règle « jamais sous 0 » n'est plus nulle part.
public function setHp(int $hp): void { $this->hp = $hp; }
// Bon : l'action du jeu, avec sa règle à l'intérieur.
public function takeDamage(int $amount): void { $this->hp = max(0, $this->hp - $amount); }
Ici, les méthodes publiques décrivent les actions du jeu : takeDamage, heal, plus tard equip et drink. En lisant vos méthodes publiques, on doit voir les règles du jeu, pas la liste de vos propriétés.
Une méthode qui lit une valeur sans permettre de l'écrire, comme hp(), reste utile : l'affichage a besoin de la barre de vie. Ce qu'on refuse, c'est l'écriture non contrôlée.
__toString : afficher un objet
echo $hero sur un objet est normalement une erreur. Déclarez __toString(): string et PHP l'appelle chaque fois que votre objet est utilisé comme du texte : echo, concaténation, interpolation dans une chaîne. C'est ce qui produit Arthur (8/10 PV) dans les sorties ci-dessus.
PHP réserve certains noms commençant par deux tirets bas, comme __construct et __toString, à des méthodes qu'il appelle automatiquement dans des situations précises. On les appelle les méthodes magiques. Ces deux-là suffisent pour l'instant.
static : parler à la classe plutôt qu'à un objet
Deuxième classe du Donjon, le dé.
<?php
declare(strict_types=1);
class Dice
{
public function __construct(
public readonly int $sides,
) {
}
public static function d6(): self
{
return new self(6);
}
public static function d20(): self
{
return new self(20);
}
public function roll(): int
{
return random_int(1, $this->sides);
}
}
$d6 = Dice::d6();
echo $d6->sides, PHP_EOL;
echo Dice::d20()->sides, PHP_EOL;
6
20
roll() a besoin d'un dé précis : sans le nombre de faces, impossible de tirer. C'est une méthode d'instance, on l'appelle avec la flèche -> sur un objet. L'appel $d6->roll() renvoie un entier entre 1 et 6, différent à chaque exécution.
d6() n'a besoin d'aucun dé existant : elle en fabrique un. C'est une méthode statique, on l'appelle avec :: sur la classe. Ici, self désigne Dice : écrire new self(6) revient à écrire new Dice(6).
Pourquoi une fabrique plutôt que new Dice(6) partout ? Parce que Dice::d6() dit ce qu'on veut obtenir. Le d6 et le d20 sont des objets du jeu, pas deux nombres quelconques.
Note
Retenez la règle de reconnaissance : :: parle à la classe, -> parle à un objet. Vous retrouverez cette notation dans Laravel, avec Post::latest() d'un côté et $post->title de l'autre. Le mécanisme interne d'Eloquent est plus subtil que notre fabrique ; seule la notation est la même.
4. Pourquoi l'anglais dans le code ?
Les identifiants de ce cours sont en anglais, les commentaires et les explications en français. Laravel est écrit en anglais : ses classes Model, Controller, User, Request, ses méthodes hasMany, latest, firstOrFail, sa documentation et ses messages d'erreur. Mélanger deux langues dans un même fichier oblige à se demander, à chaque ligne, dans quelle langue on écrit. Une règle, pas de cas particulier : identifiants en anglais, commentaires en français.
5. Hero et Dice en UML
Un dessin se lit plus vite qu'un fichier. Voici les deux classes du chapitre en UML. Le chapitre 3 apprend à produire et à lire ces diagrammes.
@startuml
class Hero {
+name: string
+strength: int
-hp: int
-maxHp: int
+hp(): int
+maxHp(): int
+takeDamage(amount: int): void
+heal(amount: int): void
+isAlive(): bool
+__toString(): string
}
class Dice {
+sides: int
+{static} d6(): Dice
+{static} d20(): Dice
+roll(): int
}
note right of Dice : d6() est soulignée : c'est une méthode statique.
@enduml
À retenir
- Un objet réunit un état, ses propriétés, et un comportement, ses méthodes. La classe est le moule,
newfabrique l'instance,$thisdésigne l'objet en cours. declare(strict_types=1), types partout, promotion de constructeur,readonly: quatre habitudes qui transforment des bugs silencieux en erreurs immédiates.- Encapsulation : les propriétés sont
private, la classe expose les actions du jeu commetakeDamage(), pas des setters commesetHp(). ::parle à la classe,->parle à un objet. Une fabrique statique commeDice::d6()dit ce qu'on veut obtenir.- Identifiants en anglais, commentaires en français : c'est la langue de Laravel.
Exercices
Les trois exercices tiennent dans un seul fichier. Créez hero.php, recopiez-y la classe Hero du chapitre, et lancez le tout avec php hero.php. Aucun outil supplémentaire n'est nécessaire : Composer et les tests arrivent au chapitre 2.
- Deux héros indépendants. Créez Arthur avec ses valeurs par défaut et Morgane avec
maxHp: 8etstrength: 3. Infligez 4 dégâts à Arthur seulement, soignez Morgane de 5, puis affichez les deux. Vérifiez que les points de vie de l'un n'ont pas bougé quand l'autre a été touché, et expliquez pourquoi en une phrase. - Une action de plus. Ajoutez à
Heroune méthodeattack(): intqui renvoie la force du héros. Affichez le résultat pour Arthur et pour Morgane. Cette méthode deviendra intéressante au chapitre 5, quand le héros pourra équiper une arme. - Deux fabriques de plus. Ajoutez la classe
Dicedans le même fichier, puis écrivezDice::d4()etDice::d100(). Comptez les lignes ajoutées. En deux phrases, dites ce qu'il faudrait changer pour créer n'importe quel dé sans écrire une méthode de plus.