Une interface, c'est un contrat
Une interface liste des méthodes sans les implémenter : c'est une promesse. Une classe qui implements une interface s'engage à fournir toutes ces méthodes. Le reste du code peut alors lui faire confiance sans connaître son type exact : c'est le polymorphisme vu dans le cours POO, en PHP.
interface MoyenPaiement {
public function payer(float $montant): string; // signature, pas de corps
}
class CarteBancaire implements MoyenPaiement {
public function payer(float $montant): string {
return "Payé $montant € par carte";
}
}
class Paypal implements MoyenPaiement {
public function payer(float $montant): string {
return "Payé $montant € via PayPal";
}
}
Une classe peut implémenter plusieurs interfaces (séparées par des virgules), contrairement à extends qui n'autorise qu'un seul parent.
Dépendre de l'interface, pas de l'implémentation
La règle d'or : une fonction doit dépendre du contrat (l'interface), pas d'une classe précise. Ainsi elle accepte n'importe quel moyen de paiement, présent ou futur.
La fonction encaisser() est typée sur l'INTERFACE MoyenPaiement, pas sur CarteBancaire ni sur Paypal. Avant de dérouler : quels objets peut-on lui passer ? Si demain on ajoute class ApplePay implements MoyenPaiement, faut-il modifier encaisser() ? Que garantit exactement implements MoyenPaiement sur une classe ?
Voir la réponse
On peut passer n'importe quel objet d'une classe qui implements MoyenPaiement : CarteBancaire, Paypal, et tout futur ApplePay. La fonction ne connaît que le contrat, pas les classes concrètes : ajouter ApplePay ne demande aucune modification de encaisser() (polymorphisme, code ouvert à l'extension). implements MoyenPaiement force la classe à fournir toutes les méthodes déclarées dans l'interface ; si une méthode manque, PHP lève une erreur fatale à la déclaration de la classe. C'est dépendre de l'interface : s'appuyer sur un contrat stable, pas sur une implémentation.
// On type avec l'INTERFACE : accepte carte, PayPal, ou tout futur moyen
function encaisser(MoyenPaiement $moyen, float $montant): void {
echo $moyen->payer($montant) . "\n";
}
encaisser(new CarteBancaire(), 50);
encaisser(new Paypal(), 30);
// Demain : une classe ApplePay implements MoyenPaiement → marche sans rien changer ici
C'est le secret du code extensible : encaisser() ne sera jamais à modifier quand on ajoute un moyen de paiement. On branche une nouvelle classe qui respecte le contrat, point.
Polymorphisme : exécute
Une seule boucle, plusieurs types : chaque objet répond à sa façon au même appel payer().
<?php
interface MoyenPaiement {
public function payer(float $montant): string;
}
class CarteBancaire implements MoyenPaiement {
public function payer(float $m): string { return "💳 $m € par carte"; }
}
class Paypal implements MoyenPaiement {
public function payer(float $m): string { return "🅿 $m € via PayPal"; }
}
class Especes implements MoyenPaiement {
public function payer(float $m): string { return "💶 $m € en espèces"; }
}
$moyens = [new CarteBancaire(), new Paypal(), new Especes()];
foreach ($moyens as $moyen) { // ignore le type exact
echo $moyen->payer(20) . "\n";
}
?>
Les enum : un type aux valeurs fixes
Comment représenter un ensemble fermé de valeurs ? Un statut de compte (actif, suspendu, supprimé), un rôle utilisateur, un jour de la semaine. Avant PHP 8.1, on utilisait des chaînes magiques ('actif') ou des constantes de classe. Le problème : une faute de frappe ('actfi') passe inaperçue, et rien n'empêche d'affecter une valeur arbitraire à une variable censée contenir un statut.
PHP 8.1 introduit enum : un type dont les seules valeurs possibles sont les case déclarés. On accède à un cas par Statut::Actif. On peut typer un paramètre function definir(Statut $s) : passer autre chose provoque une erreur. C'est sûr et auto-documenté.
<?php
// Enum "adossé" (backed) : chaque case a une valeur scalaire associée (string ou int)
enum Statut: string {
case Actif = 'actif';
case Suspendu = 'suspendu';
case Supprime = 'supprime';
public function label(): string {
return match($this) {
Statut::Actif => 'Compte actif',
Statut::Suspendu => 'Compte suspendu',
Statut::Supprime => 'Compte supprimé',
};
}
}
$s = Statut::Actif;
echo $s->value . "\n"; // actif (valeur scalaire)
echo $s->label() . "\n"; // Compte actif
// Reconstruire depuis une valeur : from() (erreur si invalide) ou tryFrom() (renvoie null)
$t = Statut::tryFrom('suspendu');
echo ($t !== null ? $t->label() : 'invalide') . "\n"; // Compte suspendu
$u = Statut::tryFrom('inconnu');
echo ($u !== null ? $u->label() : 'valeur invalide') . "\n"; // valeur invalide
?>
Le lien avec cette leçon : un enum peut avoir des méthodes et implémenter une interface. Il reste un objet à part entière, et PHP garantit qu'il respecte le contrat.
<?php
interface Affichable {
public function label(): string;
}
enum Statut: string implements Affichable {
case Actif = 'actif';
case Suspendu = 'suspendu';
case Supprime = 'supprime';
public function label(): string {
return match($this) {
Statut::Actif => 'Compte actif',
Statut::Suspendu => 'Compte suspendu',
Statut::Supprime => 'Compte supprimé',
};
}
}
// Une fonction typée sur l'interface accepte n'importe quel Affichable,
// y compris un cas d'enum : polymorphisme sans classe concrète
function afficher(Affichable $item): void {
echo $item->label() . "\n";
}
foreach (Statut::cases() as $cas) { // cases() liste tous les cas
afficher($cas);
}
?>
✦ Bonne pratique. Remplacez les chaînes magiques et les constantes de statut par un enum : impossible de passer une valeur hors de l'ensemble autorisé, zéro faute de frappe silencieuse, et le code est auto-documenté. Ajoutez une méthode label() pour les libellés lisibles, et implements une interface si plusieurs types doivent partager ce comportement.
An interface is a contract
An interface lists methods without implementing them: it's a promise. A class that implements an interface commits to providing all those methods. The rest of the code can then trust it without knowing its exact type — that's the polymorphism from the OOP course, in PHP.
interface MoyenPaiement {
public function payer(float $montant): string; // signature, no body
}
class CarteBancaire implements MoyenPaiement {
public function payer(float $montant): string {
return "Paid $montant € by card";
}
}
class Paypal implements MoyenPaiement {
public function payer(float $montant): string {
return "Paid $montant € via PayPal";
}
}
A class can implement several interfaces (comma-separated) — unlike extends which allows only one parent.
Code against the interface, not the implementation
The golden rule: a function should depend on the contract (the interface), not a specific class. That way it accepts any payment method, present or future.
The function encaisser() is typed on the INTERFACE MoyenPaiement, not on CarteBancaire or Paypal. Before scrolling: what objects can you pass it? If tomorrow you add class ApplePay implements MoyenPaiement, must you modify encaisser()? What exactly does implements MoyenPaiement guarantee on a class?
See the answer
You can pass any object of a class that implements MoyenPaiement: CarteBancaire, Paypal, and any future ApplePay. The function only knows the contract, not the concrete classes: adding ApplePay requires no change to encaisser() (polymorphism, code open to extension). implements MoyenPaiement forces the class to provide every method declared in the interface; if a method is missing, PHP raises a fatal error when the class is declared. That's coding against the interface: depending on a stable contract, not an implementation.
// We type with the INTERFACE: accepts card, PayPal, or any future method
function encaisser(MoyenPaiement $moyen, float $montant): void {
echo $moyen->payer($montant) . "\n";
}
encaisser(new CarteBancaire(), 50);
encaisser(new Paypal(), 30);
// Tomorrow: a class ApplePay implements MoyenPaiement → works with no change here
That's the secret of extensible code: encaisser() will never need changing when you add a payment method. You plug in a new class that honors the contract, done.
Polymorphism: run it
One loop, several types: each object answers the same payer() call in its own way.
<?php
interface MoyenPaiement {
public function payer(float $montant): string;
}
class CarteBancaire implements MoyenPaiement {
public function payer(float $m): string { return "💳 $m € by card"; }
}
class Paypal implements MoyenPaiement {
public function payer(float $m): string { return "🅿 $m € via PayPal"; }
}
class Especes implements MoyenPaiement {
public function payer(float $m): string { return "💶 $m € in cash"; }
}
$moyens = [new CarteBancaire(), new Paypal(), new Especes()];
foreach ($moyens as $moyen) { // ignores the exact type
echo $moyen->payer(20) . "\n";
}
?>
Enums: a type with a fixed set of values
How do you represent a closed set of values? An account status (active, suspended, deleted), a user role, a day of the week. Before PHP 8.1, we used magic strings ('active') or class constants. The problem: a typo ('actve') goes unnoticed, and nothing prevents assigning an arbitrary value to a variable supposed to hold a status.
PHP 8.1 introduces enum: a type whose only possible values are the declared cases. You access a case with Status::Active. You can type a parameter function set(Status $s): passing anything else raises an error. It's type-safe and self-documenting.
<?php
// Backed enum: each case carries a scalar value (string or int)
enum Status: string {
case Active = 'active';
case Suspended = 'suspended';
case Deleted = 'deleted';
public function label(): string {
return match($this) {
Status::Active => 'Account active',
Status::Suspended => 'Account suspended',
Status::Deleted => 'Account deleted',
};
}
}
$s = Status::Active;
echo $s->value . "\n"; // active (scalar value)
echo $s->label() . "\n"; // Account active
// Reconstruct from a value: from() (error if invalid) or tryFrom() (returns null)
$t = Status::tryFrom('suspended');
echo ($t !== null ? $t->label() : 'invalid') . "\n"; // Account suspended
$u = Status::tryFrom('unknown');
echo ($u !== null ? $u->label() : 'invalid value') . "\n"; // invalid value
?>
The link to this lesson: an enum can have methods and implement an interface. It remains a full-fledged object, and PHP enforces the contract.
<?php
interface Displayable {
public function label(): string;
}
enum Status: string implements Displayable {
case Active = 'active';
case Suspended = 'suspended';
case Deleted = 'deleted';
public function label(): string {
return match($this) {
Status::Active => 'Account active',
Status::Suspended => 'Account suspended',
Status::Deleted => 'Account deleted',
};
}
}
// A function typed on the interface accepts any Displayable,
// including an enum case: polymorphism without a concrete class
function display(Displayable $item): void {
echo $item->label() . "\n";
}
foreach (Status::cases() as $case) { // cases() lists all cases
display($case);
}
?>
✦ Best practice. Replace magic strings and status constants with an enum: impossible to pass a value outside the allowed set, zero silent typos, and the code is self-documenting. Add a label() method for human-readable labels, and implements an interface if several types need to share that behavior.
🎯 Pratique
S'entraîner (clique pour ouvrir) :
🧠 Rappel libre
Sans remonter dans la leçon : à quoi sert le mot-clé implements, et pourquoi typer un paramètre avec MoyenPaiement plutôt qu'avec CarteBancaire ?
implements engage une classe à fournir toutes les méthodes promises par l'interface : c'est un contrat vérifié par PHP (sinon erreur). Typer avec MoyenPaiement au lieu de CarteBancaire permet à la fonction d'accepter n'importe quelle classe respectant le contrat (PayPal, espèces, un futur ApplePay) sans jamais la modifier : c'est dépendre de l'interface, pas de l'implémentation.⚖️ Juge le code de l'IA
Tu demandes à l'IA un moyen de paiement « chèque ». Elle te rend ceci. Ton rôle de relecteur : l'accepter tel quel ou le rejeter, et dire pourquoi.
class Cheque extends CarteBancaire {
public function payer(float $m): string {
return "🧾 $m € par chèque";
}
}
Cheque hérite de CarteBancaire alors qu'un chèque n'est pas une carte bancaire ; il ne réutilise rien d'elle et risque d'hériter d'un comportement qui n'a aucun sens (frais de carte, etc.). Le bon geste : implémenter directement le contrat avec class Cheque implements MoyenPaiement. On hérite pour un vrai lien « est-un », on implements pour partager un contrat entre classes sans parenté.class CarteBancaire implements MoyenPaiement ?function encaisser(MoyenPaiement $m) est typée sur l'interface. On ajoute class ApplePay implements MoyenPaiement. Que peut-on passer à encaisser(), et faut-il la modifier ?PHP n'autorise qu'un seul parent (extends). Alors comment partager un même bout de code entre des classes sans lien de parenté ? Avec les traits.
Leçon 5 : Les traits →