POO et PHP, Partie 1

Héritage, encapsulation, ..., les bases de la programmation orientée objet sous PHP

Dans l'optique d'étudier la réalisation d'un framework web sous PHP, nous faisons aujourd'hui un détour pour évoquer les bases de la programmation orientée objet.
Apres un introduction en guise d'épisode 1, nous attaquons en effet l'épisode 2 qui sera un rappel pour certains, une découverte pour d'autres mais qui n'en constitue pas moins un prérequis pour la suite.
Cet épisode est un peu dense, aussi il a été découpé en 2 parties.
Commençons sans plus tarder.

Classe et objet

Pour utiliser une image très grossière, la classe peut être vue comme un moule et l'objet comme le gâteau qu'il en sortira. D'un même moule, plusieurs gâteaux peuvent être réalisés. Par ailleurs le gâteau hérite de la forme que le moule lui aura donné. Le moule en lui-même n'a pas, à proprement parlé, de cycle de vie defini dans le temps contrairement au gâteau qui, lui, a bien un instant ti de création et tf de disparition.
La comparaison s'arrête la et, bien qu'elle soit très approximative je l'avoue, elle donne déjà une bonne base de départ.

Pour reprendre les termes adéquats, on dira qu'une classe est instanciée. Il en résulte un objet. Celui-ci va hériter des attributs et des méthodes (nous reparlerons de ces termes) de la classe.
Voyons cela par un exemple.

Nous allons procéder par étape successive afin d'introduire les concepts les uns après les autres. Nous allons, pour illustrer nos propos, travailler sur des personnages de jeux.
Posons un premier jet :


class Personnage {

    public $nom;
    public $niveau;
    
    function __construct($nom = "inconnu") {
        $this->niveau = 10;
        $this->nom = $nom;
        echo nl2br("Bienvenue $nom !\n");
    }
    
}

$persoA = new Personnage();
$persoB = new Personnage('Conan');

Commentons ce code :
Nous avons créé une classe Personnage. Cette classe possède 2 attributs, $nom et $niveau. Ces derniers sont précédés du mot-clé public, nous verrons ce que cela signifie par la suite. La classe Personnage possède également une méthode, celle-ci est particulière dans la mesure ou il s'agit du constructeur. C'est en fait la méthode qui va être appelée lorsque nous allons instancier la classe.
Dans ce constructeur, nous nous contentons d'initialiser nos 2 attributs. Ainsi par défaut, le niveau de tout nouveau personnage sera 10 et son nom sera 'inconnu'.
Le mot-clé $this fait référence à l'objet propriétaire de l'attribut, qui est l'instance elle-même.
Notez par ailleurs qu'une variable $nom a également été précisée dans la signature du constructeur. Cela signifie qu'il est possible de spécifier un nom autre que 'inconnu' lors de la création du personnage.
La dernière ligne de ce code est l'instanciation, c'est à dire la création d'un objet. Nous instancions pour l'exemple 2 personnages persoA et persoB. Le code ci-dessus va afficher ceci s'il est exécuté :

Bienvenue inconnu !
Bienvenue Conan !

Les Attributs

Dans l'exemple ci-dessus les attributs $niveau et $nom ont été déclarés en public. Cela signifie qu'on peut y accéder (en lecture comme en écriture) depuis l'extérieur de la classe. Ainsi nous pourrions renommer notre persoA en accédant directement à son nom :


$persoA->nom = 'Hercule';

Nous pourrions néanmoins vouloir empêcher cela et choisir, par exemple, de ne modifier le nom que par l'intermédiaire d'une méthode et non en direct. Cela porte un nom, il s'agit de l'encapsulation.Il peut y avoir maintes raisons à cela mais il s'agit avant tout d'avoir un meilleur contrôle sur les attributs d'une classe.
Modifions donc notre code, et passons l'attribut $nom en private. Cela signifie que l'instruction ci-dessus ($persoA->nom) ne va plus fonctionner. Il est en effet impossible d'accéder a un attribut private en dehors de la classe, seule une méthode de la classe elle-même sera à même de le faire. Ajoutons également cette méthode :


class Personnage {

    private $nom;
    public $niveau;
    
    function __construct($nom = "inconnu") {
        $this->niveau = 10;
        $this->nom = $nom;
        echo nl2br("Bienvenue $nom !\n");
    }
    
    public function setNom($nom = "inconnu") {
        $this->nom = $nom;
    }
    
    public function getNom() {
        echo nl2br("Je m'appelle $this->nom !\n");
    }
}

$persoA = new Personnage();
$persoA->setNom('Hercule');
$persoA->getNom();

L'attribut $nom est donc passé en private et nous avons créé, non pas une mais deux nouvelles méthodes getNom et setNom, qui vont nous servir à, respectivement, lire et modifier le nom de notre personnage. Ces méthodes se nomment accesseur (getter en anglais) et mutateur (setter en anglais).
Suite à la déclaration de la classe Personnage, nous déroulons le scenario suivant : instanciation d'un personnage sans préciser de nom (il s'appelle donc inconnu), mise à jour de son nom (il s'appelle désormais Hercule) et lecture/affichage de son nouveau nom.
Voici le résultat :

Bienvenue inconnu !
Je m'appelle Hercule !

Héritage

Nous avons déjà vu beaucoup de concepts mais nous sommes encore loin du compte. Imaginons en effet que, dans notre jeu, nous voulions plusieurs classes de personnages (par exemple un barbare, un sorcier, etc ...). Comment pourrions-nous nous y prendre ?
Une solution serait d'ajouter un attribut catégorie par exemple. Ceci serait pertinent si toutes les catégories de personnages avaient les mêmes caractéristiques (comprenez attributs), or un barbare aura, par exemple, une jauge de puissance et un sorcier aura une jauge de magie.
Nous sentons bien que notre classe Personnage est trop générique, il nous faut la dériver en deux sous-classes Barbare et Sorcier plus spécifiques aux caractéristiques de chacun.
Doit-on conserver la classe Personnage ? Oui, car dans la mesure ou le barbare comme le sorcier auront un nom et un niveau, notre classe Personnage renfermera les attributs communs. Les classes spécifiques seront, comme leur nom l'indique, propres à chaque catégorie. Les classes Barbare et Sorcier vont hériter des attributs et méthodes de la classe Personnage. Nous appelons cela l'héritage.

PHP framework web POO programmation objet encapsulation heritage classe

Voici ce quoi vont ressembler nos 2 nouvelles classes :


class Barbare extends Personnage {
    private $puissance;

    function __construct($nom = "inconnu", $puissance = 0) {
        parent::__construct($nom);
        $this->puissance = $puissance;
        echo nl2br("Vous etes un barbare $this->nom !\n");
    }
}

class Sorcier extends Personnage {
    private $magie;

    function __construct($nom = "inconnu", $magie = 0) {
        parent::__construct($nom);
        $this->magie = $magie;
        echo nl2br("Vous etes un sorcier $this->nom !\n");
    }
}

Nous avons créé deux nouvelles classes, Barbare et Sorcier. Toutes les deux ont héritées de la classe Personnage, via le mot-clé extends. Chacune des deux classes a son propre attribut, respectivement $puissance et $magie. Ces attributs sont par ailleurs initialisés dans le constructeur de chaque classe. On constate dans chaque constructeur qu'on "construit" également la classe parent avant toute chose.
Le code ci-dessus, néanmoins, ne fonctionnera pas. Nous appelons en effet $this->nom et, d'une part nous venons de positionner l'attribut $nom en private et d'autre part, $this fait référence à l'instance en cours, si bien que $this->getNom() ne fonctionnerait pas non plus. Alors que faire ?

private, public et protected

Nous avons vu que l'encapsulation servait à maitriser nos attributs et nos méthodes. Nous ne voulons pas, en effet, d'appels directs sauvages en lecture / écriture, néanmoins une classe qui hérite d'une autre doit pouvoir y accéder. Pour ce faire il existe un troisième mode de gestion de contexte qui est protected. Concrètement, avec ce mode, seules les sous-classes peuvent accéder aux attributs ou aux méthodes qui y sont affectées.
Voici notre code complet :


class Personnage {

    protected $nom;
    public $niveau;
    
    function __construct($nom = "inconnu") {
        $this->niveau = 10;
        $this->nom = $nom;
        echo nl2br("Bienvenue $nom !\n");
    }
    
    public function setNom($nom = "inconnu") {
        $this->nom = $nom;
    }
    
    public function getNom() {
        echo nl2br("Je m'appelle $this->nom !\n");
    }
}

class Barbare extends Personnage {
    private $puissance;

    function __construct($nom = "inconnu", $puissance = 0) {
        parent::__construct($nom);
        $this->puissance = $puissance;
        echo nl2br("Vous etes un barbare $this->nom !\n");
    }
}

class Sorcier extends Personnage {
    private $magie;

    function __construct($nom = "inconnu", $magie = 0) {
        parent::__construct($nom);
        $this->magie = $magie;
        echo nl2br("Vous etes un sorcier $this->nom !\n");
    }
}

$persoA = new Barbare('Hercule');
$persoB = new Sorcier('Merlin');

Nous créons cette fois un barbare et un sorcier. Voici le résultat :

Bienvenue Hercule !
Vous etes un barbare Hercule !
Bienvenue Merlin !
Vous etes un sorcier Merlin !

Abstraction

Nous avons avancé, néanmoins tel que le code vous est proposé, il semble que nous puissions toujours instancier la classe Personnage comme nous l'avions fait au tout début. Est-ce encore utile ? Bien sûr non, au contraire il faut éviter de le faire et pour cela il suffit de préciser que Personnage est désormais une classe dite abstraite, c'est à dire qui ne sera jamais instanciée en objet.
Pour ce faire nous allons ajouter le mot-clé abstract devant le mot-clé class comme ceci :


abstract class Personnage {
 ...
}

Code définitif

Sur le dernier code, il subsiste quelques incohérences. Nous avons volontairement mis l'attribut $nom en mode protected pour illustrer son action. Néanmoins l’idéal serait de le laisser en private, et faire de même avec l'attribut $niveau.
Pour que les classes filles Barbare et Sorcier puissent accéder au nom et au niveau du personnage il faudrait placer les accesseurs et mutateurs en mode protected. Nous obtenons le code ci-dessous :


abstract class Personnage {

    private $nom;
    private $niveau;
    
    function __construct($nom = "inconnu") {
        $this->niveau = 10;
        $this->nom = $nom;
        echo nl2br("Bienvenue $nom !\n");
    }
    
    protected function setNom($nom = "inconnu") {
        $this->nom = $nom;
    }
    
    protected function getNom() {
        return $this->nom;
    }
}

class Barbare extends Personnage {
    private $puissance;

    function __construct($nom = "inconnu", $puissance = 0) {
        parent::__construct($nom);
        $this->puissance = $puissance;
        echo nl2br("Vous etes un barbare " . parent::getNom() . "!\n");
    }
}

class Sorcier extends Personnage {
    private $magie;

    function __construct($nom = "inconnu", $magie = 0) {
        parent::__construct($nom);
        $this->magie = $magie;
        echo nl2br("Vous etes un sorcier " . parent::getNom() . "!\n");
    }
}

$persoA = new Barbare('Hercule');
$persoB = new Sorcier('Merlin');

Une petite pause ...

Recapitulons ce que nous avons vu au cours de cette première partie :
- une classe est instanciée en objet,
- une classe est composée d'attributs et de méthodes, dont une qui constitue son constructeur,
- l'accès aux attributs et méthodes d'une classe peut être contrôlé par les modes public, private et protected, c'est l'encapsulation,
- public signifie que l'attribut ou la méthode est accessible depuis l'extérieur,
- private signifie que l'attribut ou la méthode est accessible uniquement depuis la classe,
- protected s'applique dans le cas d'un héritage de classe, la sous-classe et elle seule a alors accès aux attributs ou méthodes concernées,
- enfin l'abstraction de classe signifie que la classe en question ne peut être instanciée.

Il nous reste à voir quelques autres notions que nous réservons à la partie 2 de cet épisode.

BONUS : Accesseur et mutateur à la volée

Vous avez surement constaté que nous n'avons pas implémenté les accesseurs et mutateurs des attributs $niveau, $puissance et $magie. Il faudrait le faire, cependant vous n'êtes pas sans savoir que dans une application de gestion ou de commerce en ligne par exemple, les attributs peuvent se compter par dizaines pour ne pas dire plus. Sommes-nous alors condamnés à écrire des dizaines de lignes de codes. Je vous rassure, il existe un moyen de générer les méthodes, à la volée. Nous allons ajouter une méthode réservée __call dans la classe mère, par ailleurs bien que l'on puisse laisser les attributs de la classe mère en private, il nous faut positionner tous nos attributs fils en mode protected. Je ne vais pas m'étendre sur le code ci-dessous, car cela sort du cadre de cet article mais il vaut mieux vous le partager car nous en aurons besoin plus tard.
Voici donc le code pour lequel seul le barbare a été instancié, le Sorcier suivant la même logique :


abstract class Personnage {

    private $nom;
    private $niveau;
    
    function __construct($nom = "inconnu") {
        $this->niveau = 10;
        $this->nom = $nom;
        echo nl2br("Bienvenue $nom !\n");
    }
    
    public function __call($method, $args)
    {
        if ( ! preg_match('/(?Pset|get)(?P[A-Z][a-zA-Z0-9_]*)/', $method, $match) ||
             ! property_exists($this, $match['property'] = lcfirst($match['property']))
        ) {
            throw new BadMethodCallException(sprintf(
                "'%s' does not exist in '%s'.", $method, get_class($this)
            ));
        }

        switch ($match['accessor']) {
            case 'get':
                return $this->{$match['property']};
            case 'set':
                if ( ! $args) {
                    throw new InvalidArgumentException(sprintf("'%s' requires an argument value.", $method));
                }
                $this->{$match['property']} = $args[0];
                return $this;
        }
    }
}

class Barbare extends Personnage {
    protected $puissance;

    function __construct($nom = "inconnu", $puissance = 0) {
        parent::__construct($nom);
        $this->puissance = $puissance;
        echo nl2br("Vous etes un barbare " .$this->getNom() . "!\n");
    }
    
    public function caracteristiques(){
        echo nl2br("\nVos caracteristiques : "     ."\n".
                   "Nom       : " .$this->getNom() ."\n".
                   "Categorie : Barbare"           ."\n".
                   "Puissance : " . $this->getPuissance());
    }
}

$persoA = new Barbare('Hercule', 7);
$persoA->caracteristiques();

Vous constatez que nous n'avons plus d'accesseur ni de mutateur dans la classe Personnage. Pourtant dans la classe Barbare nous avons ajouté une méthode "caractéristiques" qui appelle tous les mutateurs des attributs (qu'ils soient propriétés de la classe mère ou de la classe fille).
C'est en effet toute la puissance d'une génération à la volée. Voici le résultat :

Bienvenue Hercule !
Vous etes un barbare Hercule!

Vos caracteristiques :
Nom : Hercule
Categorie : Barbare
Puissance : 7

Crédits

Crédits des illustrations :
Sorcier : Wizard icons created by Smashicons - Flaticon
Barbare : Thor icons created by Smashicons - Flaticon


Retrouvez dans la rubrique "Nos datasets" toutes les données dont vous aurez besoin pour tester et pratiquer !