T: / Articles techniques / PHP
Comment lier les objets entre eux, par le typage ou par l’héritage.
On continue la découverte de la programmation orientée objet en PHP avec des façons de lier les classes entre elles, par le typage ou par l’héritage. Juste avant cela, on verra la notion de constantes de classes qui pourra nous aider à bien structurer notre code.
Au programme :
On va placer dans le contexte d’un jeu vidéo médiéval-fantasy dans lequel on a différents types de personnage. Chaque personnage a différentes caractéristiques, peuvent réaliser certaines actions, et peuvent collecter des items (pour ne pas dire objet…) au cours de leur aventure.
Voici comment illustrer un personnage :
class Personnage
{
private int $pointsDeVie;
private array $items = [];
private int $nbItems = 0;
public function __construct(
public readonly string $nom,
public readonly int $force,
public readonly int $magie,
public readonly int $defense,
) {
$this->pointsDeVie = 100;
}
}
Un peu d’explications :
Voici ce que ça peut donner dans mon programme principal :
$personnage = new Personnage('Gandalf', 10, 20, 5);
Comme on crée des constantes avec la fonction define() pour représenter des éléments de contexte de notre programme qui sont figés, on peut créer des constantes dans les classes.
Cela permet à la fois de ranger les constantes dans la classe qui leur correspond mais aussi de profiter des notions de visibilité liée à la POO.
Dans mon jeu vidéo, je vais avoir 3 types de personnages : des guerriers, des magiciens et des archers. Je vais représenter ces 3 types par 3 constantes. Voici comment mon code évolue :
class Personnage
{
// Constantes de classe
public const TYPE_GUERRIER = 'guerrier';
public const TYPE_MAGICIEN = 'magicien';
public const TYPE_ARCHER = 'archer';
// Propriétés
private int $pointsDeVie;
private array $items = [];
private int $nbItems = 0;
public function __construct(
public readonly string $nom,
// On va stocker le type du personnage dans une propriété
public readonly string $type,
public readonly int $force,
public readonly int $magie,
public readonly int $defense,
) {
$this->pointsDeVie = 100;
}
}
Dans mon programme principal, je vais pouvoir faire appel à ces constantes depuis l’extérieur de mon objet comme elles sont public :
// Je ne fais pas :
$personnage = new Personnage('Gandalf', 'magicien', 10, 20, 5);
// Je fais :
$personnage = new Personnage('Gandalf', Personnage::TYPE_MAGICIEN, 10, 20, 5);
// On peut le réécrire avec les paramètres nommés pour une meilleure lisibilité
$personnage = new Personnage(
nom: 'Gandalf',
type: Personnage::TYPE_MAGICIEN,
force: 10,
magie: 20,
defense: 5
);
On a vu comment accéder à une constante depuis l’extérieur de la classe, mais on peut aussi y accéder depuis l’intérieur. Imaginons que chaque type de personnage a une attaque spéciale :
public function attaqueSpeciale(): int
{
return match ($this->type) {
self::TYPE_GUERRIER => $this->force * 2,
self::TYPE_MAGICIEN => $this->magie * 2,
self::TYPE_ARCHER => $this->force + $this->magie,
default => 0,
};
}
Un peu d’explications :
On va maintenant gérer les items de notre Personnage. On peut :
On peut donc créer ces méthodes dans Personnage :
// Dans Personnage.php
public function ajouterItem(string $item, int $quantite = 1): void
{
// On boucle pour ajouter l'item X fois
for ($i = 0; $i < $quantite; $i++) {
$this->items[] = $item;
}
}
public function retirerItem(string $item, int $quantite = 1): void
{
// On boucle pour retirer l'item X fois
for ($i = 0; $i < $quantite; $i++) {
// On vérifie si l'item est présent, via array_search
$key = array_search($item, $this->items);
if ($key !== false) {
// On retire l'item
unset($this->items[$key]);
}
}
}
Et une méthode qui sert à compter les items. Cette méthode n’ aucun intérêt à être appelé de l’extérieur donc je vais la mettre en private :
private function compterItems(): void
{
$this->nbItems = count($this->items);
}
// Et on appelle cette méthode dans les 2 autres :
public function ajouterItem(string $item, int $quantite = 1): void
{
// Code précédent...
$this->compterItems();
}
public function retirerItem(string $item, int $quantite = 1): void
{
// Code précédent...
$this->compterItems();
}
Et c’est là qu’on peut aborder le principe de responsabilité unique en POO.
En fait, tout ce code, ce n’est pas au Personnage de le gérer directement, on peut tout mettre dans une classe dédiée Inventaire qui sera elle chargée de gérer les items.
Le personnage pourra toujours ajouter ou retirer des objets, mais pas les compter, il n’y a pas besoin qu’il gère ça en direct.
Créons donc cette nouvelle classe Inventaire :
class Inventaire
{
private array $items = [];
private int $nbItems = 0;
// On n'a pas de constructeur, ce n'est pas obligatoire
// Ajout d'un ou plusieurs items (le même item X fois)
public function ajouterItem(string $item, int $quantite = 1): void
{
// On boucle pour ajouter l'item X fois
for ($i = 0; $i < $quantite; $i++) {
$this->items[] = $item;
}
$this->compterItems();
}
// Retirer un ou plusieurs items (le même item X fois)
public function retirerItem(string $item, int $quantite = 1): void
{
// On boucle pour retirer l'item X fois
for ($i = 0; $i < $quantite; $i++) {
// On vérifie si l'item est présent, via array_search
$key = array_search($item, $this->items);
if ($key !== false) {
// On retire l'item
unset($this->items[$key]);
}
}
$this->compterItems();
}
private function compterItems(): void
{
$this->nbItems = count($this->items);
}
// On rajoute un getter
public function getNbItems(): int
{
return $this->nbItems;
}
}
Et maintenant les évolutions dans la classe Personnage. On n’a plus besoin de $items et $nbItems qui sont passées dans Inventaire. A la place, on va pouvoir mettre directement un inventaire et le typé comme tel : (je ne remets pas toute la classe)
class Personnage
{
// Propriété typée avec l'autre classe
private int $pointsDeVie;
private Inventaire $inventaire;
public function ajouterItem(string $item, int $quantite = 1): void
{
$this->inventaire->ajouterItem($item, $quantite);
}
public function retirerItem(string $item, int $quantite = 1): void
{
$this->inventaire->retirerItem($item, $quantite);
}
}
Voici un premier moyen de lier les objets entre eux en PHP.
Un Personnage a un Inventaire. En typant ma propriété $inventaire de cette façon, j’indique à mon code clairement que cette propriété contiendra une instance de la classe Inventaire.
Et on retrouve donc cette logique de « cascade » :
$this->inventaire->ajouterItem($item, $quantite);
Les flèches s’enchainent pour passer d’un objet à un autre.
Tu peux continuer à creuser ce concept avec ce corrigé qui propose un refactoring de code en appliquant ce principe.
Jusqu’ici, on a manipulé des méthodes d’instance : pour les appeler, on doit d’abord créer un personnage. Mais il pourrait y avoir certaines règles du jeu qui ne dépendent d’aucun personnage en particulier.
C’est là que les méthodes statiques entrent en jeu !
Exemple d’une méthode statique
Imaginons une classe responsable des règles de combat :
class CombatRules
{
public static function calculerDegats(int $attaque, int $defense): int
{
$degats = $attaque - $defense;
return max(0, $degats);
}
}
Cette méthode :
Globalement, quand utiliser une méthode statique ?
Et pour s’en servir :
// Pas besoin de faire un new
$degats = CombatRules::calculerDegats(15, 10);
Les méthodes statiques sont souvent utilisées pour créer des helpers : des classes utilitaires qui centralisent des traitements communs, sans avoir besoin de créer d’instances.
On a 3 types de personnage, je peux donc faire des choses comme ça :
$magicien = new Personnage(
nom: 'Gandalf',
type: Personnage::TYPE_MAGICIEN,
force: 10,
magie: 20,
defense: 5
);
$guerrier = new Personnage(
nom: 'Aragorn',
type: Personnage::TYPE_GUERRIER,
force: 20,
magie: 5,
defense: 10
);
$archer = new Personnage(
nom: 'Legolas',
type: Personnage::TYPE_ARCHER,
force: 10,
magie: 10,
defense: 5
);
Mais si je pouvais faire des choses comme ça, ce serait quand même plus simple :
$magicien = new Magicien('Gandalf');
$guerrier = new Guerrier('Aragorn');
$archer = new Archer('Legolas');
Facile ! Je copie/colle ma classe Personnage 3 fois dans 3 nouvelles classes (Magicien, Guerrier et Archer) et je définis des valeurs par défaut sur mes propriétés et le tour est joué !
Sauf que… Si je veux rajouter une nouvelle méthode ou propriété, je vais devoir la mettre dans 3 fichiers différents… Pareil pour les mises à jour…. Et si je veux un nouveau type de personnage… Ca va devenir le bazar…
C’est là que l’héritage entre en jeu !
Dans le vocabulaire, on va dire que la classe Magicien hérite (ou étend) de la classe Personnage. De cette façon, tout ce qui se trouve dans la classe Personnage est automatiquement disponible pour la classe Magicien (seulement si public ou protected).
Voici notre classe Magicien :
class Magicien extends Personnage
{
public function __construct(string $nom, int $magie = 20)
{
parent::__construct(
// Gérable
nom: $nom,
magie: $magie,
// Définies par défaut et communes pour toutes les instances
type: Personnage::TYPE_MAGICIEN,
force: 10,
defense: 5
);
}
}
Un peu d’explications :
On pourrait créer plein de Magicien :
$gandalf = new Magicien('Gandalf', 50);
$harry = new Magicien('Harry Potter');
$david = new Magicien('David Copperfield', 5);
Et je n’ai plus besoin, comme au début, de repréciser le type de Personnage, certaines caractéristiques, etc.
Et j’ai accès à toutes les méthodes public du parent :
// Ce code est tout à fait fonctionnel
$gandalf->ajouterItem('Anneau');
Je n’ai pas créé de méthode ajouterItem dans la classe Magicien, mais j’en ai bien une dans la classe Personnage qui est son parent.
Je peux par contre compléter ma classe avec des méthodes ou propriétés qui lui sont spécifiques. Un exemple avec la classe Archer :
class Archer extends Personnage
{
private int $visee = 20;
public function __construct(string $nom, int $defense = 20)
{
parent::__construct(
// Gérable
nom: $nom,
defense: $defense,
// Définis par défaut
type: Personnage::TYPE_ARCHER,
magie: 5,
force: 20
);
}
public function tirALArc(): int
{
return $this->visee + $this->defense * 2;
}
}
// ------------
$legolas = new Archer('Legolas', 30);
$gandalf = new Magicien('Gandalf', 50);
// Ce code fonctionne :
$legolas->tirALArc();
// Ce code ne fonctionne pas :
$gandalf->tirALArc(); // Magicien n'a pas la méthode tirALArc()
Il faut tout de même faire attention lorsqu’on crée des méthodes spécifiques à des classes enfant et ne pas s’emmêler à appeler une méthode sur une classe alors que cette classe ne l’a pas.
Pour se protéger de tout cela et aller encore plus loin dans les liens entre les objets, il faudra voir les notions d’interfaces ou de méthodes abstraites.
N’hésite pas à choisir un challenge de code et à le résoudre en POO pour mettre en pratique ces éléments.
Other content to discover