La programmation orientée objet en PHP
Un élève, un compte en banque, une connexion à une base de données : en programmation orientée objet, on décrit une sorte de chose une fois, dans une classe, puis on crée autant d'objets qu'il en faut. Chacun garde ses propres données et sait faire ce que sa classe prévoit. PHP fournit d'ailleurs ses propres classes, comme PDO, qui parle aux bases de données.
Après cette fiche, tu sais
- écrire une classe avec propriétés typées, constructeur et méthodes, et créer des objets avec
new; - prévoir ce que fait
$b = $aavec un objet, et quand utiliserclone; - protéger les données avec
private,protectedet des méthodes qui valident ; - employer l'héritage et les interfaces, et interroger une base avec PDO et des requêtes préparées.
Une classe décrit, les objets existent
Une classe commence par le mot-clé class, puis son nom, puis des accolades qui contiennent ses propriétés (les données) et ses méthodes (les fonctions). new crée un objet, une instance de la classe ; dans une méthode, $this désigne l'objet sur lequel elle est appelée.1
<?php
class Eleve
{
public string $prenom;
public array $notes = [];
public function __construct(string $prenom)
{
$this->prenom = $prenom;
}
public function ajouterNote(float $note): void
{
$this->notes[] = $note;
}
public function moyenne(): float
{
return round(array_sum($this->notes) / count($this->notes), 2);
}
}
$lea = new Eleve('Léa');
$lea->ajouterNote(5.5);
$lea->ajouterNote(5);
$noah = new Eleve('Noah');
$noah->ajouterNote(3);
echo $lea->prenom, ' : ', $lea->moyenne(), "\n";
echo $noah->prenom, ' : ', $noah->moyenne(), "\n";
Léa : 5.25 Noah : 3
Le plan et les objets
Fais défiler : le code ci-dessus s'exécute ligne après ligne. Deux objets naissent du même plan, et chacun garde ses notes.
Un plan, pas encore d'élève
Une variable propre à chaque objet, déclarée dans la classe ; on peut la typer (string, array…) depuis PHP 7.4.1
Une fonction de la classe, appelée sur un objet avec -> : $lea->moyenne().1
__construct() est appelée automatiquement sur chaque nouvel objet : l'endroit pour l'initialiser.1
Depuis PHP 8.0, public function __construct(public string $prenom) {} déclare la propriété et l'initialise en une ligne.1
Dans une méthode, $notes[] = $note; crée une variable locale $notes, qui disparaît à la fin de la méthode : la propriété de l'objet ne change pas. Pour toucher la propriété, il faut $this->notes[] = $note;.
Appelée sur un élève sans note, moyenne() divise par count([]), soit 0 : DivisionByZeroError. Le corrigé 2 montre comment l'éviter.
$b = $a ne copie pas l'objet
Une variable « objet » ne contient pas l'objet lui-même, seulement un identifiant qui permet de le retrouver. Affecter l'objet à une autre variable, le passer à une fonction ou le renvoyer copie cet identifiant : les deux variables désignent alors le même objet.3 Pour un objet indépendant, on écrit clone.3
<?php
class Eleve
{
public function __construct(public string $prenom) {}
}
$a = new Eleve('Léa');
$b = $a; // le même objet
$b->prenom = 'Lina';
echo $a->prenom, "\n";
$c = clone $a; // un nouvel objet, copie de $a
$c->prenom = 'Zoé';
echo $a->prenom, "\n";
var_dump($a === $b, $a === $c);
Lina Lina bool(true) bool(false)
Des flèches vers les objets
Fais défiler : chaque variable est une étiquette qui pointe vers un objet numéroté, comme dans var_dump (object(Eleve)#1).
Un objet, une variable
Vrai seulement s'il s'agit de la même instance.
La fonction reçoit une copie de l'identifiant : si elle change une propriété, l'objet de l'appelant change.3
Copie superficielle : les propriétés sont copiées, mais un objet rangé dans une propriété reste partagé, sauf si la méthode __clone() le clone à son tour.3
Rappel de la fiche précédente : un tableau affecté ou passé à une fonction est copié. Les objets se comportent autrement.
Que va afficher ce code ?
Quatre extraits, tous exécutés avec PHP 8.4. La classe Eleve est celle de l'exemple ci-dessus.
public, protected, private : qui a le droit d'y toucher
Un membre public est accessible partout ; protected, seulement dans la classe, ses classes enfants et ses parents ; private, seulement dans la classe qui le déclare.1 On garde les données private et on n'expose que des méthodes qui les vérifient : c'est l'encapsulation.
<?php
class Compte
{
private float $solde = 0;
public function deposer(float $montant): void
{
if ($montant <= 0) {
throw new InvalidArgumentException('Montant positif attendu');
}
$this->solde += $montant;
}
public function retirer(float $montant): void
{
if ($montant > $this->solde) {
throw new RuntimeException('Solde insuffisant');
}
$this->solde -= $montant;
}
public function solde(): float
{
return $this->solde;
}
}
$compte = new Compte();
$compte->deposer(200);
$compte->retirer(50);
echo $compte->solde(), "\n";
try {
$compte->retirer(500);
} catch (RuntimeException $e) {
echo 'Refusé : ', $e->getMessage(), "\n";
}
$compte->solde = 1000000; // interdit depuis l'extérieur
150 Refusé : Solde insuffisant Fatal error: Uncaught Error: Cannot access private property Compte::$solde
Trois portes, trois points de vue
Fais défiler : on regarde la même classe depuis l'extérieur, depuis une classe enfant, puis depuis la classe elle-même.
Depuis l'extérieur
Une méthode refuse une valeur impossible en lançant une exception ; l'appelant la rattrape avec try … catch.
Depuis PHP 8.1, une propriété readonly s'initialise une seule fois, dans sa classe ; toute modification ensuite lève Cannot modify readonly property.1
Compte peut lire le private $solde d'un autre objet Compte.1Accepté ou refusé ?
Compte a un private $solde, un protected $titulaire et une méthode publique solde() ; CompteEpargne extends Compte ; Forme est une interface. Chaque ligne a été testée avec PHP 8.4.
extends : hériter et spécialiser
Une classe enfant hérite de toutes les méthodes et propriétés public et protected de son parent ; elle peut en ajouter, et redéfinir une méthode héritée. Les membres private du parent lui restent inaccessibles.2
<?php
class Compte
{
protected float $solde = 0;
public function deposer(float $montant): void
{
$this->solde += $montant;
}
public function solde(): float
{
return $this->solde;
}
}
class CompteEpargne extends Compte
{
public function __construct(private float $taux) {}
public function ajouterInterets(): void
{
$this->deposer(round($this->solde * $this->taux, 2));
}
}
$e = new CompteEpargne(0.0125); // taux fictif de 1,25 %
$e->deposer(1000);
$e->ajouterInterets();
echo $e->solde(), "\n";
var_dump($e instanceof Compte);
1012.5 bool(true)
La classe enfant éclatée
Fais défiler : les membres hérités descendent de Compte dans CompteEpargne, puis servent.
Deux classes
Une méthode redéfinie peut appeler la version du parent : parent::deposer($montant). Si l'enfant a son propre constructeur, celui du parent n'est appelé que par parent::__construct().1
$e instanceof Compte vaut true : un compte d'épargne est un compte.
Une classe final ne peut pas avoir d'enfant ; PHP refuse class Fille extends Base si Base est final.
Un enfant peut assouplir une visibilité (protected vers public), jamais la restreindre.2
Si $solde était private dans Compte, $this->solde dans CompteEpargne ne la verrait pas : Warning: Undefined property: CompteEpargne::$solde. C'est à cela que sert protected.2
Un contrat, plusieurs façons de le remplir
Une interface dit quelles méthodes une classe doit avoir, sans dire comment elles marchent. Une classe qui l'implements doit les écrire toutes, sinon PHP refuse la classe ; elle peut implémenter plusieurs interfaces.2 Une fonction qui attend l'interface accepte alors n'importe quel objet qui la respecte : c'est le polymorphisme.
<?php
interface Forme
{
public function aire(): float;
}
class Rectangle implements Forme
{
public function __construct(private float $l, private float $h) {}
public function aire(): float
{
return $this->l * $this->h;
}
}
class Cercle implements Forme
{
public function __construct(private float $r) {}
public function aire(): float
{
return round(M_PI * $this->r ** 2, 2);
}
}
function aireTotale(array $formes): float
{
$total = 0;
foreach ($formes as $f) {
$total += $f->aire(); // chaque objet répond à sa façon
}
return $total;
}
echo aireTotale([new Rectangle(3, 4), new Cercle(1)]);
15.14
Fais défiler : la même ligne $f->aire() exécute deux méthodes différentes.
Rectangle et Cercle implements Forme : chacune a sa méthode aire(), écrite à sa façon.2
$f est un Rectangle : $f->aire() exécute la méthode de Rectangle, 3 × 4 = 12.
$f est un Cercle : la même ligne exécute la méthode de Cercle, π × 1², arrondi à 3,14.
aireTotale renvoie 15,14 sans jamais savoir quelles classes existent : il lui suffit que chaque objet respecte le contrat Forme.
| interface | classe abstraite | |
|---|---|---|
| Contient du code | non, seulement des signatures de méthodes | oui, des méthodes complètes et des méthodes abstraites |
| Instanciable | non | non |
| Une classe peut en avoir | plusieurs (implements A, B) | une seule (extends) |
| Quand l'utiliser | pour un contrat commun à des classes sans lien | pour partager du code entre classes proches |
Une classe abstraite ne peut pas être instanciée, et toute classe qui contient une méthode abstraite doit l'être ; ses enfants doivent écrire toutes les méthodes abstraites.2 PHP répond Cannot instantiate interface Forme à new Forme().
Un objet de tous les jours : PDO
PDO (PHP Data Objects) est une classe fournie par PHP pour parler aux bases de données : MySQL, PostgreSQL, SQLite… On crée un objet PDO, puis on prépare des requêtes. Depuis PHP 8.0, une erreur SQL lève par défaut une PDOException.4
Une requête préparée contient des marqueurs (:classe ou ?) ; les valeurs sont transmises à part, avec execute(). La base distingue alors toujours le code SQL des données : une saisie ne peut pas changer la structure de la requête.45
<?php
$pdo = new PDO('sqlite::memory:'); // MySQL : 'mysql:host=localhost;dbname=ecole;charset=utf8mb4'
$pdo->exec("CREATE TABLE eleves (id INTEGER PRIMARY KEY, prenom TEXT, classe TEXT)");
$pdo->exec("INSERT INTO eleves (prenom, classe) VALUES ('Léa', '2M3'), ('Noah', '2M3'), ('Elias', '2M1')");
$classe = $_GET['classe'] ?? '2M3'; // vient de l'URL : jamais collé dans le SQL
$stmt = $pdo->prepare('SELECT prenom FROM eleves WHERE classe = :classe ORDER BY prenom');
$stmt->execute(['classe' => $classe]);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $ligne) {
echo htmlspecialchars($ligne['prenom']), "\n";
}
Léa Noah
La saisie qui change le sens de la requête
Fais défiler : la même saisie malveillante, d'abord collée dans le SQL, puis passée à une requête préparée. Tout a été exécuté avec PDO et SQLite.
La saisie
<?php
$pdo = new PDO('sqlite::memory:');
$pdo->exec("CREATE TABLE eleves (id INTEGER PRIMARY KEY, prenom TEXT, classe TEXT)");
$pdo->exec("INSERT INTO eleves (prenom, classe) VALUES ('Léa', '2M3'), ('Noah', '2M3'), ('Elias', '2M1')");
$classe = "' OR '1'='1";
// DANGER : la saisie est collée dans la requête
$sql = "SELECT prenom FROM eleves WHERE classe = '$classe' ORDER BY prenom";
foreach ($pdo->query($sql) as $ligne) {
echo $ligne['prenom'], "\n";
}
// Requête préparée : la saisie reste une valeur
$stmt = $pdo->prepare('SELECT prenom FROM eleves WHERE classe = :classe ORDER BY prenom');
$stmt->execute(['classe' => $classe]);
echo count($stmt->fetchAll()), " ligne(s)\n";
Elias Léa Noah 0 ligne(s)
Un marqueur remplace une valeur, jamais un nom de table, de colonne ou un ordre de tri (ASC, DESC). Si l'utilisateur choisit la colonne de tri, on compare son choix à une liste de valeurs autorisées.5
Requête sûre ?
Trois questions sur PDO et l'injection SQL.
Sept bonnes pratiques
- une classe, une responsabilité ; un nom au singulier (
Eleve,Compte) ; - propriétés
privateet typées par défaut,readonlyquand elles ne doivent plus changer ; - valider dans les méthodes et lancer une exception pour une valeur impossible ;
protectedseulement quand un enfant en a vraiment besoin ;- se souvenir que
$b = $apartage l'objet, et utiliserclonepour une copie ; - typer les paramètres par une interface (
Forme $f) plutôt que par une classe précise ; - avec PDO : toujours
prepare()etexecute()pour les valeurs, jamais de concaténation.
Trois exercices corrigés
Tous les corrigés ont été exécutés avec PHP 8.4.
1Écris une classe Rectangle avec un constructeur promu (largeur, hauteur) et les méthodes aire() et perimetre().Voir le corrigé
<?php
class Rectangle
{
public function __construct(
private float $largeur,
private float $hauteur,
) {}
public function aire(): float
{
return $this->largeur * $this->hauteur;
}
public function perimetre(): float
{
return 2 * ($this->largeur + $this->hauteur);
}
}
$r = new Rectangle(3, 4.5);
echo $r->aire(), ' / ', $r->perimetre();
13.5 / 15
Les deux propriétés sont private : on ne peut plus fausser une mesure depuis l'extérieur. La virgule après le dernier paramètre est permise depuis PHP 8.0.
2Améliore Eleve : notes privées, refus d'une note hors de l'échelle 1 à 6, et une moyenne qui vaut null quand il n'y a encore aucune note.Voir le corrigé
<?php
class Eleve
{
private array $notes = [];
public function __construct(public string $prenom) {}
public function ajouterNote(float $note): void
{
if ($note < 1 || $note > 6) {
throw new InvalidArgumentException("Note $note hors de l'échelle 1 à 6");
}
$this->notes[] = $note;
}
public function moyenne(): ?float
{
if (count($this->notes) === 0) {
return null;
}
return round(array_sum($this->notes) / count($this->notes), 2);
}
}
$zoe = new Eleve('Zoé');
var_dump($zoe->moyenne());
$zoe->ajouterNote(6);
$zoe->ajouterNote(5.5);
var_dump($zoe->moyenne());
try {
$zoe->ajouterNote(7);
} catch (InvalidArgumentException $e) {
echo $e->getMessage();
}
NULL float(5.75) Note 7 hors de l'échelle 1 à 6
?float annonce « un float ou null ». La division par zéro ne peut plus arriver, et une note impossible n'entre jamais dans l'objet.
3Cette ligne cherche la classe d'un élève à partir d'un prénom saisi. Pourquoi est-elle dangereuse, et comment la corriger ?Voir le corrigé
$ligne = $pdo->query("SELECT classe FROM eleves WHERE prenom = '$prenom'")->fetch();
La saisie est collée dans le SQL : une apostrophe suffit à changer la requête. Version corrigée, avec un marqueur positionnel :
<?php
$pdo = new PDO('sqlite::memory:');
$pdo->exec("CREATE TABLE eleves (id INTEGER PRIMARY KEY, prenom TEXT, classe TEXT)");
$pdo->exec("INSERT INTO eleves (prenom, classe) VALUES ('Léa', '2M3'), ('Noah', '2M3'), ('Elias', '2M1')");
$prenom = 'Noah';
$stmt = $pdo->prepare('SELECT classe FROM eleves WHERE prenom = ?');
$stmt->execute([$prenom]);
$ligne = $stmt->fetch(PDO::FETCH_ASSOC);
echo $ligne ? $ligne['classe'] : 'introuvable';
2M3
La fiche en huit lignes
Teste-toi
Sources
- The PHP Group, PHP Manual, « Classes and Objects » : « The Basics », « Properties » (types, readonly), « Constructors and Destructors » (promotion, parent::__construct), « Visibility ». bases, propriétés, constructeurs, visibilité (php.net)
- The PHP Group, PHP Manual : « Object Inheritance », « Object Interfaces », « Class Abstraction ». héritage, interfaces, classes abstraites (php.net)
- The PHP Group, PHP Manual : « Objects and references », « Object Cloning ». références, clone (php.net)
- The PHP Group, PHP Manual, PDO : « Prepared statements », « Errors and error handling » (ERRMODE_EXCEPTION par défaut depuis PHP 8.0). requêtes préparées, erreurs (php.net)
- OWASP, SQL Injection Prevention Cheat Sheet (requêtes paramétrées, limites pour les noms de tables et de colonnes). cheatsheetseries.owasp.org
Fiche écrite et sourcée en octobre 2026. Tout le code a été exécuté avec PHP 8.4 ; les requêtes, avec PDO et SQLite.