Introduction à la programmation orientée objet
Présentation
Jusqu'ici, vos programmes PHP se construisent autour de fonctions et de variables. Vous écrivez des instructions qui manipulent des données passées en paramètres, et chaque fonction fait son travail de manière isolée. Cette approche fonctionne bien tant que le code reste court et que les données restent simples.
Dès qu'un programme manipule plusieurs entités du monde réel, chacune avec ses propres informations et ses propres traitements, le code procédural commence à s'étirer. Les fonctions se multiplient. Les mêmes tableaux associatifs circulent d'un fichier à l'autre. Une modification dans la structure d'une donnée oblige à vérifier partout où cette donnée est lue ou écrite.
La programmation orientée objet propose une autre manière d'organiser un programme. Plutôt que de séparer les données d'un côté et les fonctions de l'autre, elle les regroupe dans des unités appelées objets. Chaque objet porte à la fois ses propres données et les fonctions qui agissent sur ces données.
Quand le procédural devient encombrant
Pour comprendre ce que la POO apporte, le plus efficace est d'observer ce qui coince sans elle. Nous allons construire une petite page qui présente un ou plusieurs champions d'un jeu. Chaque champion possède un nom, un rôle, des points de vie et des dégâts.
Commençons avec un seul champion, écrit en PHP procédural tel que vous le connaissez déjà.
Fichier index.php
<?php
$nomChampion = 'Lux';
$roleChampion = 'Mage';
$pointsDeVieChampion = 100;
$degatsChampion = 60;
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Premier champion</title>
</head>
<body>
<h1>Champion sélectionné</h1>
<div>
<h2><?= $nomChampion ?></h2>
<p>Rôle : <?= $roleChampion ?></p>
<p>Points de vie : <?= $pointsDeVieChampion ?></p>
<p>Dégâts : <?= $degatsChampion ?></p>
</div>
</body>
</html>
Ce code fonctionne sans difficulté. Nous avons quatre variables qui décrivent un champion, puis nous les affichons dans la page. Tant qu'il n'y en a qu'un seul, rien n'accroche.
Voyons ce qui se passe lorsque la page doit afficher deux champions.
Fichier index.php
<?php
$nomChampion1 = 'Lux';
$roleChampion1 = 'Mage';
$pointsDeVieChampion1 = 100;
$degatsChampion1 = 60;
$nomChampion2 = 'Garen';
$roleChampion2 = 'Combattant';
$pointsDeVieChampion2 = 140;
$degatsChampion2 = 45;
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Deux champions</title>
</head>
<body>
<h1>Champions sélectionnés</h1>
<div>
<h2><?= $nomChampion1 ?></h2>
<p>Rôle : <?= $roleChampion1 ?></p>
<p>Points de vie : <?= $pointsDeVieChampion1 ?></p>
<p>Dégâts : <?= $degatsChampion1 ?></p>
</div>
<div>
<h2><?= $nomChampion2 ?></h2>
<p>Rôle : <?= $roleChampion2 ?></p>
<p>Points de vie : <?= $pointsDeVieChampion2 ?></p>
<p>Dégâts : <?= $degatsChampion2 ?></p>
</div>
</body>
</html>
Le code fonctionne toujours, mais plusieurs choses commencent à gêner.
D'abord, chaque champion est décrit par quatre variables distinctes. Pour deux champions, nous en avons déjà huit. Pour cinq, nous en aurions vingt.
Ensuite, les informations d'un même champion sont éparpillées. Rien dans le code n'exprime que $nomChampion1, $roleChampion1, $pointsDeVieChampion1 et $degatsChampion1 forment un ensemble cohérent. Ce lien n'existe que dans la tête du développeur.
Enfin, si nous décidons plus tard d'ajouter une cinquième information, par exemple une image ou du mana, il faut déclarer une nouvelle variable pour chaque champion existant, et penser à l'afficher partout où chaque champion est présenté. Plus les champions se multiplient, plus cette opération devient fragile.
Ce qui est intéressant dans ce code, c'est que les quatre variables suivent toujours le même schéma. Chaque champion possède un nom, un rôle, des points de vie et des dégâts. Ce qui change d'un champion à l'autre, ce sont les valeurs. Ce qui reste identique, c'est la structure.
C'est exactement dans ce genre de situation que la POO prend tout son sens. Elle permet de décrire cette structure commune une seule fois, puis de fabriquer autant d'objets concrets que l'on souhaite à partir de cette description.
Décrire une structure commune avec une classe
En programmation orientée objet, la classe est l'outil qui sert à décrire une structure commune. Une classe n'est pas un champion. Elle est le modèle à partir duquel on pourra fabriquer autant de champions que nécessaire.
L'analogie la plus simple est celle du plan d'architecte. Un plan n'est pas une maison. C'est la description de ce qu'une maison construite à partir de ce plan devra contenir. À partir d'un seul plan, on peut ensuite construire plusieurs maisons, toutes identiques dans leur structure, mais pouvant différer par leurs occupants, leurs couleurs ou leur mobilier.
Écrivons donc une première classe Champion.
Fichier index.php
<?php
class Champion
{
public string $nom = '';
public string $role = '';
public int $pointsDeVie = 100;
public int $degats = 0;
}
Cette classe déclare quatre propriétés. Une propriété est une donnée qui appartiendra à chaque champion fabriqué à partir de cette classe. Autrement dit, n'importe quel futur champion aura un nom, un rôle, des points de vie et des dégâts.
Le mot-clé public placé devant chaque propriété fait partie de la syntaxe imposée par PHP. Il précise qui peut lire et modifier ces propriétés depuis l'extérieur de l'objet. Pour l'instant, retenez simplement que public autorise un accès libre. Nous reviendrons précisément sur ce point dans le chapitre consacré à l'encapsulation.
Remarquez aussi que certaines propriétés reçoivent déjà une valeur dans la classe, par exemple $pointsDeVie = 100. Il s'agit d'une valeur de départ. Lorsqu'un champion sera fabriqué à partir de cette classe, il recevra d'office cette valeur, à moins qu'on ne la modifie ensuite.
À ce stade, la classe seule ne produit rien. Elle ne correspond à aucun champion précis. Elle décrit simplement ce qu'un champion devra contenir. Pour obtenir un vrai champion, il faut construire un objet à partir de cette classe.
Créer un premier objet avec new
Pour passer du plan à une maison concrète, il faut construire. En POO, l'équivalent de cette construction s'appelle l'instanciation. Nous fabriquons alors un objet, également appelé une instance de la classe.
PHP réalise cette fabrication grâce au mot-clé new.
Fichier index.php
<?php
class Champion
{
public string $nom = '';
public string $role = '';
public int $pointsDeVie = 100;
public int $degats = 0;
}
$champion1 = new Champion();
$champion1->nom = 'Lux';
$champion1->role = 'Mage';
$champion1->degats = 60;
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Premier objet</title>
</head>
<body>
<h1>Champion sélectionné</h1>
<div>
<h2><?= $champion1->nom ?></h2>
<p>Rôle : <?= $champion1->role ?></p>
<p>Points de vie : <?= $champion1->pointsDeVie ?></p>
<p>Dégâts : <?= $champion1->degats ?></p>
</div>
</body>
</html>
La première ligne intéressante est celle-ci.
$champion1 = new Champion();
Elle fabrique un nouvel objet à partir de la classe Champion, puis le stocke dans la variable $champion1. On dit aussi que nous avons instancié la classe.
Les lignes qui suivent remplissent les propriétés de cet objet. L'opérateur ->, appelé opérateur d'accès aux membres, permet d'atteindre une propriété précise de l'objet.
$champion1->nom = 'Lux';
Cette ligne se lit ainsi. La propriété nom de l'objet contenu dans $champion1 reçoit la valeur 'Lux'.
Dans la partie HTML de la page, nous lisons ces propriétés avec la même syntaxe, par exemple $champion1->nom pour afficher le nom du champion.
Créer plusieurs objets à partir d'une même classe
L'intérêt réel d'une classe apparaît dès qu'il faut fabriquer plusieurs objets.
Fichier index.php
<?php
class Champion
{
public string $nom = '';
public string $role = '';
public int $pointsDeVie = 100;
public int $degats = 0;
}
$champion1 = new Champion();
$champion1->nom = 'Lux';
$champion1->role = 'Mage';
$champion1->degats = 60;
$champion2 = new Champion();
$champion2->nom = 'Garen';
$champion2->role = 'Combattant';
$champion2->pointsDeVie = 140;
$champion2->degats = 45;
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Deux objets</title>
</head>
<body>
<h1>Champions sélectionnés</h1>
<div>
<h2><?= $champion1->nom ?></h2>
<p>Rôle : <?= $champion1->role ?></p>
<p>Points de vie : <?= $champion1->pointsDeVie ?></p>
<p>Dégâts : <?= $champion1->degats ?></p>
</div>
<div>
<h2><?= $champion2->nom ?></h2>
<p>Rôle : <?= $champion2->role ?></p>
<p>Points de vie : <?= $champion2->pointsDeVie ?></p>
<p>Dégâts : <?= $champion2->degats ?></p>
</div>
</body>
</html>
Les deux variables $champion1 et $champion2 contiennent maintenant deux objets distincts. Ces objets ont été construits à partir de la même classe. Ils partagent donc la même structure, à savoir les mêmes propriétés dans le même ordre. En revanche, chacun contient ses propres valeurs. L'un est Lux, l'autre est Garen, et leurs points de vie ainsi que leurs dégâts diffèrent.
Nous tenons ici l'une des idées centrales de la POO. Une classe décrit ce qui est commun. Les objets portent chacun leurs propres valeurs.
La classe Champion n'est donc ni Lux ni Garen. Elle est le modèle qui a permis de fabriquer ces deux objets distincts. Rien ne nous empêcherait d'en créer un troisième, un dixième ou un centième, sans réécrire la moindre ligne de classe.
Comparez mentalement ce code avec la version procédurale du début du chapitre. Les informations d'un même champion sont maintenant regroupées dans un objet unique. La structure n'a été décrite qu'une seule fois. Ajouter une nouvelle propriété, comme une image, se ferait désormais à un seul endroit, à l'intérieur de la classe.
Associer un comportement à un objet avec une méthode
Un objet ne sert pas uniquement à stocker des données. Il peut aussi contenir des traitements qui ont un rapport direct avec ces données. Dans le vocabulaire de la POO, un traitement déclaré dans une classe s'appelle une méthode.
Une méthode ressemble fortement à une fonction, avec deux différences importantes. Elle vit à l'intérieur d'une classe, et elle peut accéder librement aux propriétés de l'objet sur lequel elle est appelée.
Ajoutons une première méthode à notre classe.
Fichier index.php
<?php
class Champion
{
public string $nom = '';
public string $role = '';
public int $pointsDeVie = 100;
public int $degats = 0;
public function obtenirPhrasePresentation(): string
{
return $this->nom . ' est un champion de type ' . $this->role . '.';
}
}
$champion1 = new Champion();
$champion1->nom = 'Lux';
$champion1->role = 'Mage';
$champion1->degats = 60;
$champion2 = new Champion();
$champion2->nom = 'Garen';
$champion2->role = 'Combattant';
$champion2->pointsDeVie = 140;
$champion2->degats = 45;
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Première méthode</title>
</head>
<body>
<h1>Champions sélectionnés</h1>
<div>
<h2><?= $champion1->nom ?></h2>
<p><?= $champion1->obtenirPhrasePresentation() ?></p>
</div>
<div>
<h2><?= $champion2->nom ?></h2>
<p><?= $champion2->obtenirPhrasePresentation() ?></p>
</div>
</body>
</html>
La méthode obtenirPhrasePresentation() construit une phrase à partir des données de l'objet, puis la renvoie avec return.
Nous l'appelons depuis le HTML de la manière suivante.
$champion1->obtenirPhrasePresentation()
La présence des parenthèses est importante. Elles distinguent une méthode d'une propriété. Sans parenthèses, nous lisons une donnée. Avec parenthèses, nous déclenchons un comportement.
Ce détail de syntaxe illustre une idée plus profonde. Dans un objet, les données et les comportements cohabitent, mais ils ne se confondent pas. Les propriétés décrivent ce que l'objet est. Les méthodes décrivent ce que l'objet sait faire.
Faire référence à l'objet courant avec $this
Dans la méthode précédente, nous avons utilisé un mot-clé particulier, $this.
return $this->nom . ' est un champion de type ' . $this->role . '.';
Ce mot-clé désigne l'objet courant, c'est-à-dire l'objet sur lequel la méthode est en train d'être appelée. Il fait donc la différence entre Lux et Garen au moment où la méthode s'exécute.
Lorsque nous écrivons cette ligne...
$champion1->obtenirPhrasePresentation();
...alors, à l'intérieur de la méthode, $this pointe vers l'objet contenu dans $champion1. L'expression $this->nom vaut donc 'Lux'.
Lorsque nous écrivons celle-ci...
$champion2->obtenirPhrasePresentation();
...alors, dans cette autre exécution, $this pointe vers l'objet contenu dans $champion2. L'expression $this->nom vaut cette fois 'Garen'.
La méthode est unique, écrite une seule fois dans la classe. Son résultat dépend pourtant de l'objet sur lequel elle est appelée. C'est ce qui permet d'écrire un comportement générique qui s'adapte automatiquement à chaque objet.
Initialiser un objet dès sa création avec un constructeur
Revenons à la manière dont nous fabriquons nos objets depuis le début du chapitre.
$champion1 = new Champion();
$champion1->nom = 'Lux';
$champion1->role = 'Mage';
$champion1->pointsDeVie = 100;
$champion1->degats = 60;
Cette manière de faire a un défaut. Rien n'oblige à remplir les propriétés juste après l'instanciation. Un objet peut donc rester dans un état incomplet, avec un nom vide ou des dégâts à zéro, et la page affichera alors des valeurs incohérentes sans rien signaler.
Par ailleurs, créer un objet en cinq lignes à chaque fois devient répétitif dès que l'on manipule plusieurs champions.
La POO propose une solution prévue pour ce cas. Elle s'appelle le constructeur. Un constructeur est une méthode spéciale, exécutée automatiquement par PHP au moment où un objet est créé. En PHP, cette méthode s'appelle toujours __construct(), avec deux caractères de soulignement au début.
Ajoutons un constructeur à notre classe.
Fichier index.php
<?php
class Champion
{
public string $nom = '';
public string $role = '';
public int $pointsDeVie = 100;
public int $degats = 0;
public function __construct(string $nom, string $role, int $pointsDeVie, int $degats)
{
$this->nom = $nom;
$this->role = $role;
$this->pointsDeVie = $pointsDeVie;
$this->degats = $degats;
}
public function obtenirPhrasePresentation(): string
{
return $this->nom . ' est un champion de type ' . $this->role . '.';
}
}
$champion1 = new Champion('Lux', 'Mage', 100, 60);
$champion2 = new Champion('Garen', 'Combattant', 140, 45);
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Constructeur</title>
</head>
<body>
<h1>Champions sélectionnés</h1>
<div>
<h2><?= $champion1->nom ?></h2>
<p>Points de vie : <?= $champion1->pointsDeVie ?></p>
<p><?= $champion1->obtenirPhrasePresentation() ?></p>
</div>
<div>
<h2><?= $champion2->nom ?></h2>
<p>Points de vie : <?= $champion2->pointsDeVie ?></p>
<p><?= $champion2->obtenirPhrasePresentation() ?></p>
</div>
</body>
</html>
Le constructeur reçoit des paramètres, comme n'importe quelle fonction ou méthode. Dans son corps, il utilise ces paramètres pour remplir les propriétés de l'objet grâce à $this.
La création d'un champion devient alors beaucoup plus directe.
$champion1 = new Champion('Lux', 'Mage', 100, 60);
Cette unique ligne fabrique l'objet et remplit toutes ses propriétés. Il n'est plus possible d'obtenir un champion à l'état vide, car le constructeur exige que les valeurs nécessaires lui soient fournies au moment du new.
Compléter le constructeur avec des valeurs par défaut
Certaines informations sont plus stables que d'autres. Par exemple, la plupart des champions commencent leur partie avec 100 points de vie. Nous pouvons alors donner une valeur par défaut à ce paramètre, exactement comme vous l'avez vu pour les fonctions classiques.
Fichier index.php
class Champion
{
public $nom = '';
public $role = '';
public $pointsDeVie = 0;
public $degats = 0;
public function __construct(string $nom, string $role, int $degats, ?int $pointsDeVie = 100)
{
$this->nom = $nom;
$this->role = $role;
$this->degats = $degats;
$this->pointsDeVie = $pointsDeVie;
}
}
$champion1 = new Champion('Lux', 'Mage', 60);
$champion2 = new Champion('Garen', 'Combattant', 45, 140);
Cette version permet d'appeler le constructeur de deux manières. Pour Lux, nous ne transmettons que trois arguments. PHP utilise alors la valeur par défaut 100 pour les points de vie. Pour Garen, nous transmettons quatre arguments. PHP utilise donc la valeur explicite 140 et ignore la valeur par défaut.
Notez que les paramètres ayant une valeur par défaut doivent être placés à la fin de la liste des paramètres. C'est pourquoi $pointsDeVie a été déplacé en dernière position.
Le constructeur devient ainsi un point de passage unique par lequel tout nouvel objet de la classe doit passer. Il garantit que chaque objet démarre dans un état cohérent, tout en laissant une certaine souplesse sur les informations qui admettent une valeur par défaut.
Synthèse
Dans cette page, nous sommes partis d'un constat. Le code procédural s'étire dès qu'il doit gérer plusieurs entités de même structure, parce que les données se dispersent et que rien n'exprime le lien entre elles.
Pour répondre à ce problème, la POO nous a donné une nouvelle façon d'organiser un programme, construite autour de sept notions.
Une classe décrit une structure commune. Elle joue le rôle d'un plan ou d'un modèle. Elle ne produit rien à elle seule.
Un objet, également appelé instance, est un exemplaire concret fabriqué à partir d'une classe grâce au mot-clé new.
Une propriété est une donnée qui appartient à un objet. Tous les objets d'une même classe ont les mêmes propriétés, mais chacun possède ses propres valeurs.
Une méthode est un comportement déclaré dans la classe, qui peut utiliser les propriétés de l'objet. Elle s'appelle avec des parenthèses, contrairement à une propriété.
Le mot-clé $this permet à une méthode d'accéder à l'objet sur lequel elle est appelée. C'est ce qui rend une même méthode capable de produire un résultat différent selon l'objet courant.
Le constructeur, méthode spéciale __construct(), est appelé automatiquement à la création d'un objet. Il permet d'initialiser proprement les propriétés en une seule étape.
Nous avons désormais toutes les briques de base pour manipuler des classes et des objets. Une question reste cependant ouverte. Faut-il vraiment autoriser le code extérieur à lire et modifier librement toutes les propriétés d'un objet, ou faut-il au contraire mieux protéger certaines d'entre elles ? C'est précisément cette question qui nous conduira au chapitre suivant, consacré à l'encapsulation.