Apprendre la POO en PHP #2 – Constantes, typage et héritage

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 :

Présentation de l’exemple qui va illustrer notre code POO

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 :

  • Il y a 2 propriétés privées qui pourront évoluer selon certaines règles. Ce sera donc la classe qui sera chargée de les faire évoluer, c’est pour ça qu’elles sont en privées.
  • $pointsDeVie est initialisée dans le constructeur
  • $items a une valeur par défaut, ainsi que $nbItems (le nombre d’items)
  • J’utilise la promotion de propriétés dans le constructeur pour à la fois déclarer et initialiser mes propriétés

Voici ce que ça peut donner dans mon programme principal :

$personnage = new Personnage('Gandalf', 10, 20, 5);

Les constantes de classes en PHP

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
);

Le mot clé self en PHP

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 :

  • J’utilise la structure match pour gérer chaque cas
  • Quand $this me permet d’atteindre un élément de mon objet (= de mon instance de classe), self permet d’accéder à la classe directement. Comme les constantes sont figées et font partie de la classe elle même, on utilise self::

Typage avancé et principe de responsabilité unique

On va maintenant gérer les items de notre Personnage. On peut :

  • Ajouter un ou plusieurs items
  • Retirer un ou plusieurs items
  • Je veux garder en mémoire combien j’ai d’items (via $nbiItems)

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.

Méthodes statiques

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 :

  • ne dépend d’aucun personnage précis
  • applique une règle commune à tous les combats
  • travaille uniquement avec les valeurs reçues

Globalement, quand utiliser une méthode statique ?

  • La méthode peut être utilisée à plusieurs endroits dans notre programme
  • Son comportement n’est pas lié à une autre classe
  • Aucune propriété n’est nécessaire pour son bon fonctionnement (pas de $this)

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.

Héritage en PHP

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 utilise le mot clé extends pour mettre en place l’héritage. Cela peut se lire « La classe Magicien étend la classe Personnage ».
  • Je crée un constructeur avec 2 paramètres (qui ne sont pas des propriétés, pas de promotion de propriétés dans mon exemple)
  • Le constructeur de Magicien fait appel au constructeur de Personnage via parent::
    En POO, la classe étendue est appelée « le parent ». Cela signifie qu’on fait appel à une méthode de la classe étendue.
  • La logique d’initialisation des propriétés est donc celle de la classe Personnage
  • J’ai des valeurs qui se gèrent donc « sur mesure » $nom et $magie (avec une valeur par défaut quand même pour $magie) et des valeurs qui seront communes pour toutes les instances de Magicien

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.


Qui a codé ce superbe contenu ?

Keep learning

Other content to discover