Leçon 3/7 10 min

Héritage et classes abstraites

extends, parent::, redéfinir une méthode, et les classes abstraites comme gabarits. L'héritage en PHP, avec du code exécutable.

extends : hériter d'une classe

Un CompteEpargne est un Compte avec un taux en plus. En PHP, on l'écrit avec extends : la classe fille récupère toutes les propriétés et méthodes public/protected du parent.

class Compte {
    protected float $solde = 0;        // protected → accessible aux filles
    public function deposer(float $m): void { if ($m > 0) $this->solde += $m; }
    public function getSolde(): float { return $this->solde; }
}

class CompteEpargne extends Compte {
    public function __construct(private float $taux) {}

    public function ajouterInterets(): void {
        $this->deposer($this->solde * $this->taux);  // réutilise la méthode héritée
    }
}

Remarque : $solde est protected (pas private) pour rester accessible à la classe fille. private resterait invisible même pour les enfants.

Hiérarchie d'héritage : Compte parent, CompteEpargne et CompteCourant enfants Compte # solde : float + deposer() + getSolde() CompteEpargne - taux : float + ajouterInterets() CompteCourant - decouvert : float + retirer() override extends extends
Les classes filles héritent des membres public/protected du parent, puis ajoutent ou redéfinissent.

parent:: et la redéfinition

Une fille peut redéfinir (override) une méthode du parent, et appeler la version parente avec parent::. Si la fille a son propre constructeur, elle doit appeler parent::__construct(...) pour initialiser la partie héritée.

Prédisez avant de lire

Imaginez ce code : class Animal { function decrire() { return "Je suis un animal"; } } et class Chien extends Animal { function decrire() { return parent::decrire() . " et un chien"; } }. Avant de dérouler : que renvoie (new Chien())->decrire(), à quoi sert parent::decrire() exactement, et que se passerait-il si on l'enlevait ?

Voir la réponse

Chien::decrire() remplace (override) la méthode héritée. parent::decrire() appelle explicitement la version d'Animal depuis l'enfant pour la compléter, au lieu de la jeter. (new Chien())->decrire() renvoie "Je suis un animal et un chien". Sans parent::, le comportement parent disparait : il ne resterait que " et un chien" (ou il faudrait tout réécrire). C'est le mécanisme pour étendre une méthode plutôt que la remplacer entièrement.

class CompteCourant extends Compte {
    public function __construct(private float $decouvertAutorise) {
        parent::__construct();   // construit la partie "Compte"
    }
    // on REDÉFINIT retirer pour autoriser un découvert
    public function retirer(float $m): bool {
        if ($m > 0 && $m <= $this->solde + $this->decouvertAutorise) {
            $this->solde -= $m;
            return true;
        }
        return false;
    }
}

Les classes abstraites : un gabarit

Une classe abstraite ne peut pas être instanciée directement : c'est un gabarit pour ses filles. Elle peut imposer des méthodes abstraites (sans corps) que chaque fille doit implémenter.

abstract class Forme {
    abstract public function aire(): float;   // pas de corps : à implémenter

    public function decrire(): string {        // méthode concrète partagée
        return "Aire = " . $this->aire();
    }
}
class Carre extends Forme {
    public function __construct(private float $cote) {}
    public function aire(): float { return $this->cote ** 2; }
}
// new Forme();  // ❌ Error: Cannot instantiate abstract class Forme

Exécute : héritage en action

<?php
class Compte {
    protected float $solde = 0;
    public function deposer(float $m): void { if ($m > 0) $this->solde += $m; }
    public function getSolde(): float { return $this->solde; }
}
class CompteEpargne extends Compte {
    public function __construct(private float $taux) {}
    public function ajouterInterets(): void {
        $this->deposer($this->solde * $this->taux);
    }
}

$e = new CompteEpargne(0.05);
$e->deposer(1000);        // méthode HÉRITÉE de Compte
$e->ajouterInterets();    // méthode PROPRE → +50
echo "Solde : " . $e->getSolde() . " €\n";  // 1050
?>

final : interdire l'héritage ou la redéfinition

L'héritage et la redéfinition sont puissants, mais parfois on veut garantir qu'une classe ou une méthode ne sera pas modifiée par une sous-classe. PHP offre le mot-clé final exactement pour ça.

  • final class Paiement { … } : la classe ne peut pas être étendue. Tenter class X extends Paiement provoque une erreur fatale à la compilation (Class X cannot extend final class Paiement).
  • final public function valider() { … } dans une classe non-finale : la méthode ne peut pas être redéfinie par une fille. La tenter provoque Cannot override final method.
<?php
// final class : aucune classe ne peut l'étendre
final class Paiement {
    public function __construct(private float $montant) {}

    public function valider(): string {
        return "Paiement de {$this->montant} € validé.";
    }
}

// Utilisation normale : fonctionne parfaitement
$p = new Paiement(49.90);
echo $p->valider() . "\n";

// Ce qui suit provoquerait une erreur fatale à la compilation :
// class PaiementFraude extends Paiement {}
// Fatal error: Class PaiementFraude cannot extend final class Paiement

// final sur une méthode dans une classe non-finale
class Compte {
    protected float $solde = 0;
    public function deposer(float $m): void { if ($m > 0) $this->solde += $m; }

    final public function getSolde(): float { return $this->solde; }
    // getSolde() ne peut PAS être redéfinie par une classe fille
}

class CompteDemo extends Compte {
    // final public function getSolde(): float { return 0; }
    // => Fatal error: Cannot override final method Compte::getSolde()
}

$c = new CompteDemo();
$c->deposer(200);
echo "Solde : " . $c->getSolde() . " €\n";
?>

Bonne pratique : final est un choix de conception délibéré, pas une obligation. Réservez-le aux classes critiques (logique de paiement, sécurité, invariants métier stricts) ou aux méthodes dont le comportement ne doit jamais varier dans les sous-classes. Évitez de mettre final partout par réflexe : cela réduit la flexibilité et complique les tests (impossible de créer un mock qui hérite).

extends: inheriting from a class

A CompteEpargne is a Compte with an extra rate. In PHP, you write it with extends: the child class gets all the parent's public/protected properties and methods.

class Compte {
    protected float $solde = 0;        // protected → accessible to children
    public function deposer(float $m): void { if ($m > 0) $this->solde += $m; }
    public function getSolde(): float { return $this->solde; }
}

class CompteEpargne extends Compte {
    public function __construct(private float $taux) {}

    public function ajouterInterets(): void {
        $this->deposer($this->solde * $this->taux);  // reuses the inherited method
    }
}

Note: $solde is protected (not private) so it stays accessible to the child class. private would be invisible even to children.

Inheritance hierarchy: Compte parent, CompteEpargne and CompteCourant children Compte # solde : float + deposer() + getSolde() CompteEpargne - taux : float + ajouterInterets() CompteCourant - decouvert : float + retirer() override extends extends
Child classes inherit the parent's public/protected members, then add or override.

parent:: and overriding

A child can override a parent method, and call the parent version with parent::. If the child has its own constructor, it must call parent::__construct(...) to initialize the inherited part.

Predict before reading

Consider this code: class Animal { function decrire() { return "Je suis un animal"; } } and class Chien extends Animal { function decrire() { return parent::decrire() . " et un chien"; } }. Before scrolling down: what does (new Chien())->decrire() return, what exactly does parent::decrire() do, and what would happen if you removed it?

See the answer

Chien::decrire() overrides (replaces) the inherited method. parent::decrire() explicitly calls the Animal version from the child to extend it, rather than throwing it away. (new Chien())->decrire() returns "Je suis un animal et un chien". Without parent::, the parent behaviour is lost: only " et un chien" would remain (or you'd have to rewrite everything). This is the mechanism to extend a method rather than replace it entirely.

class CompteCourant extends Compte {
    public function __construct(private float $decouvertAutorise) {
        parent::__construct();   // builds the "Compte" part
    }
    // we OVERRIDE retirer to allow an overdraft
    public function retirer(float $m): bool {
        if ($m > 0 && $m <= $this->solde + $this->decouvertAutorise) {
            $this->solde -= $m;
            return true;
        }
        return false;
    }
}

Abstract classes: a template

An abstract class can't be instantiated directly: it's a template for its children. It can require abstract methods (no body) that each child must implement.

abstract class Forme {
    abstract public function aire(): float;   // no body: must be implemented

    public function decrire(): string {        // shared concrete method
        return "Area = " . $this->aire();
    }
}
class Carre extends Forme {
    public function __construct(private float $cote) {}
    public function aire(): float { return $this->cote ** 2; }
}
// new Forme();  // ❌ Error: Cannot instantiate abstract class Forme

Run it: inheritance in action

<?php
class Compte {
    protected float $solde = 0;
    public function deposer(float $m): void { if ($m > 0) $this->solde += $m; }
    public function getSolde(): float { return $this->solde; }
}
class CompteEpargne extends Compte {
    public function __construct(private float $taux) {}
    public function ajouterInterets(): void {
        $this->deposer($this->solde * $this->taux);
    }
}

$e = new CompteEpargne(0.05);
$e->deposer(1000);        // method INHERITED from Compte
$e->ajouterInterets();    // OWN method → +50
echo "Balance: " . $e->getSolde() . " €\n";  // 1050
?>

final: prevent inheritance or overriding

Inheritance and overriding are powerful, but sometimes you want to guarantee that a class or method won't be changed by a subclass. PHP provides the final keyword exactly for that.

  • final class Paiement { … }: the class cannot be extended. Attempting class X extends Paiement triggers a fatal compile-time error (Class X cannot extend final class Paiement).
  • final public function valider() { … } in a non-final class: the method cannot be overridden by a child. Any attempt throws Cannot override final method.
<?php
// final class: no class can extend it
final class Paiement {
    public function __construct(private float $montant) {}

    public function valider(): string {
        return "Payment of {$this->montant} € validated.";
    }
}

// Normal use: works perfectly
$p = new Paiement(49.90);
echo $p->valider() . "\n";

// The following would cause a fatal compile-time error:
// class PaiementFraude extends Paiement {}
// Fatal error: Class PaiementFraude cannot extend final class Paiement

// final on a method inside a non-final class
class Compte {
    protected float $solde = 0;
    public function deposer(float $m): void { if ($m > 0) $this->solde += $m; }

    final public function getSolde(): float { return $this->solde; }
    // getSolde() CANNOT be overridden by any child class
}

class CompteDemo extends Compte {
    // final public function getSolde(): float { return 0; }
    // => Fatal error: Cannot override final method Compte::getSolde()
}

$c = new CompteDemo();
$c->deposer(200);
echo "Balance: " . $c->getSolde() . " €\n";
?>

Best practice: final is a deliberate design choice, not a default. Reserve it for critical classes (payment logic, security, strict business invariants) or methods whose behaviour must never vary in subclasses. Avoid marking everything final by reflex: it reduces flexibility and makes testing harder (you can't create a mock that inherits).

🎯 Pratique

S'entraîner (clique pour ouvrir) :

⚖️ Juge le code de l'IA
Accepter ou rejeter le code de l'IA

Tu veux un compte de support avec un plafond de retrait. L'IA te propose de faire hériter CompteSupport de CompteEpargne et passe $solde en public. Tu acceptes tel quel, ou tu rejettes ? Justifie.

class CompteSupport extends CompteEpargne {  // CompteEpargne a déjà ajouterInterets()
    public float $solde = 0;                 // public : on y accède de partout
    public function retirer(float $m): void {
        if ($m <= 1000) $this->solde -= $m;  // plafond codé en dur
    }
}
À rejeter. Deux fautes. 1) public float $solde casse l'encapsulation : n'importe quel code peut écrire $compte->solde = 999999 sans passer par deposer(). Le parent l'avait à juste titre en protected ; ici on l'expose pour rien. 2) Héritage abusif : un compte de support n'est pas un compte épargne, il hérite donc d'ajouterInterets() qui n'a aucun sens pour lui. La règle « est-un » n'est pas respectée. Le bon réflexe : garder $solde en protected et faire hériter CompteSupport de Compte (ou d'une classe/interface adaptée), pas d'CompteEpargne.
🧠 Rappel libre
Rappel libre

Sans remonter dans la leçon : pourquoi met-on $solde en protected plutôt qu'en private dans Compte, et qu'est-ce qui empêche d'écrire new Forme() sur une classe abstraite ?

protected rend $solde accessible à la classe fille (ex. CompteEpargne qui lit $this->solde dans ajouterInterets()) ; private le rendrait invisible même pour les enfants. Pour la classe abstraite : abstract class Forme ne peut pas être instanciée parce qu'elle déclare au moins une méthode abstraite sans corps (aire()) ; PHP lève « Cannot instantiate abstract class Forme ». Elle sert de gabarit : seules ses filles, qui implémentent aire(), sont instanciables.
Quel mot-clé fait hériter une classe d'une autre en PHP ?
Une classe abstraite…
Dans une classe fille qui définit son propre constructeur, à quoi sert l'appel parent::__construct(...) ?
Une abstract class Forme déclare abstract public function aire(): float;. Que doivent faire les classes Cercle et Carre qui l'étendent, et pourquoi est-il impossible d'écrire new Forme() ?
Prochaine étape

L'abstraction impose un contrat. PHP a un outil dédié pour ça, plus souple que l'héritage : les interfaces.

Leçon 4 : Interfaces →

Une erreur dans cette leçon, un passage flou, une question ? Écrivez-moi : chaque retour améliore ce cours.

Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement