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.

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 !