Tu as déjà croisé une exception sans la comprendre
À la leçon 2, dans le setter de Produit, on a écrit ceci sans s'arrêter dessus :
public function setPrix(float $prix): void {
if ($prix < 0) {
throw new InvalidArgumentException("Prix négatif interdit");
}
$this->prix = $prix;
}
Ce throw new InvalidArgumentException, il est temps de comprendre ce qu'il fait vraiment. Il répond à une question qui revient sans cesse : que fait une méthode quand elle ne PEUT PAS faire son travail ? Un prix négatif, un solde trop bas, un fichier absent.
La mauvaise réponse, c'est le code d'erreur. retirer() renvoie false quand ça échoue. Une autre méthode renvoie null. Une troisième renvoie -1. L'appelant doit deviner la convention de chacune, et surtout penser à tester le retour à chaque appel. Un oubli, et le programme continue avec une valeur fausse. C'est le chaos des codes d'erreur.
false ou un null de retour ne force personne à le regarder. Le code qui ignore l'erreur compile, tourne, et propage des données corrompues en silence. C'est exactement le bug qu'une exception rend impossible à ignorer.
throw : arrêter net avec un objet qui raconte tout
Lever une exception, c'est dire « je ne peux pas continuer » et arrêter la méthode immédiatement. À la ligne du throw, l'exécution de la méthode s'interrompt : les lignes suivantes ne tournent pas. PHP remonte alors la pile d'appels à la recherche de quelqu'un pour gérer le problème.
Et ce n'est pas un simple message : une exception est un objet. Comme tout objet (leçon 1), elle porte des données et des méthodes. Elle sait raconter ce qui s'est passé :
$e->getMessage(): le message lisible que tu as écrit.$e->getFile()et$e->getLine(): le fichier et la ligne exacts duthrow.$e->getCode(): un code numérique optionnel.$e->getTrace(): toute la pile d'appels qui a mené là.
function diviser(int $a, int $b): float {
if ($b === 0) {
throw new InvalidArgumentException("Division par zéro interdite");
}
return $a / $b; // sauté si b vaut 0
}
Si personne ne l'attrape, l'exception devient une erreur fatale et le script s'arrête. Ce n'est pas un défaut : mieux vaut un arrêt net et bruyant qu'un résultat faux qui se propage. La suite montre comment l'attraper proprement.
try / catch : attraper le problème
Pour gérer une exception au lieu de planter, on entoure le code risqué d'un bloc try, suivi d'un catch qui récupère l'objet exception. Le flux d'exécution est précis :
- Le code du
trys'exécute normalement, ligne après ligne. - Si un
throwsurvient (ici ou dans une méthode appelée), PHP saute tout le reste dutryet bondit dans lecatch. - Le
catchreçoit l'objet exception, le traite, puis l'exécution reprend normalement après le bloc. - Si aucun
throwne survient, lecatchest tout simplement ignoré.
try {
$r = diviser(10, 0);
echo "Résultat : $r"; // sauté : le throw a eu lieu avant
} catch (InvalidArgumentException $e) {
echo "Souci : " . $e->getMessage(); // on récupère le contrôle ici
}
echo "\nLe programme continue."; // exécuté dans tous les cas
Et finally ? Un bloc optionnel après le catch, exécuté quoi qu'il arrive : succès, exception attrapée, ou même exception relancée. C'est l'endroit du nettoyage qu'on ne veut jamais oublier : fermer un fichier, libérer une connexion à la base.
Lever et attraper : exécute
Un compte qui refuse un retrait à découvert, non plus avec un false muet (leçon 2), mais avec une exception qui explique le problème. Note bien : le programme continue après le catch.
<?php
class Compte {
private float $solde = 0;
public function deposer(float $m): void { $this->solde += $m; }
public function retirer(float $m): void {
if ($m > $this->solde) {
throw new RuntimeException(
"Solde insuffisant : il manque " . ($m - $this->solde) . " €"
);
}
$this->solde -= $m;
}
public function getSolde(): float { return $this->solde; }
}
$c = new Compte();
$c->deposer(50);
try {
$c->retirer(200); // lève l'exception
echo "Retrait effectué\n"; // sauté
} catch (RuntimeException $e) {
echo "Erreur : " . $e->getMessage() . "\n";
} finally {
echo "Solde : " . $c->getSolde() . " €\n"; // toujours exécuté
}
echo "Le programme continue normalement.\n";
?>
La hiérarchie : du spécifique au général
Toutes les exceptions descendent d'une seule classe de base : Exception. PHP fournit ensuite une famille toute prête, les exceptions SPL, qui en héritent :
InvalidArgumentException: un argument reçu est invalide (prix négatif, format faux).RuntimeException: un problème survenu à l'exécution (solde trop bas, ressource indisponible).LogicException: une erreur de logique du programme, qu'on aurait dû éviter en codant.
Et c'est ici que l'héritage de la leçon 3 paie comptant. Un catch attrape une classe et toutes ses classes filles : c'est du polymorphisme. Comme InvalidArgumentException hérite (indirectement) d'Exception, un catch (Exception $e) attrape aussi les InvalidArgumentException.
Conséquence directe : on attrape du plus spécifique au plus général. Un catch trop large placé en premier raflerait tout, et les catch plus précis en dessous ne serviraient jamais.
<?php
function valider(int $age): void {
if ($age < 0) throw new InvalidArgumentException("Âge négatif");
if ($age > 150) throw new RuntimeException("Âge irréaliste");
}
foreach ([-5, 200, 30] as $age) {
try {
valider($age);
echo "$age : valide\n";
} catch (InvalidArgumentException $e) { // le plus spécifique d'abord
echo "$age : argument invalide (" . $e->getMessage() . ")\n";
} catch (Exception $e) { // le filet de sécurité, en dernier
echo "$age : autre erreur (" . $e->getMessage() . ")\n";
}
}
?>
catch (Exception $e) avant catch (InvalidArgumentException $e), le premier attrape tout (l'héritage joue) et le second devient du code mort. Toujours du spécifique vers le général.
Tes propres exceptions : du métier dans le nom
Réutiliser une exception SPL suffit souvent. Mais parfois, le nom de l'erreur doit parler le langage de ton domaine. CommandeIntrouvableException raconte le problème métier bien mieux qu'un RuntimeException générique. Et créer la sienne est trivial : il suffit d'étendre Exception (encore l'héritage de la leçon 3).
<?php
// Une exception métier : son nom dit déjà tout
class CommandeIntrouvableException extends Exception {}
class Catalogue {
private array $commandes = ["CMD-1" => 49.90, "CMD-2" => 12.00];
public function montant(string $ref): float {
if (!isset($this->commandes[$ref])) {
throw new CommandeIntrouvableException("Commande $ref introuvable");
}
return $this->commandes[$ref];
}
}
$cat = new Catalogue();
foreach (["CMD-1", "CMD-9"] as $ref) {
try {
echo "$ref : " . $cat->montant($ref) . " €\n";
} catch (CommandeIntrouvableException $e) { // on cible précisément ce cas
echo "Souci métier : " . $e->getMessage() . "\n";
}
}
?>
La règle de décision : réutilise une SPL tant qu'elle décrit bien le problème (argument invalide, état d'exécution). Crée la tienne dès que tu veux l'attraper de façon ciblée, ou quand son nom apporte une information métier que l'appelant doit pouvoir distinguer.
if et un message à l'écran. « La base de données est injoignable » est anormal : là, l'exception est légitime. Lever une exception pour piloter un déroulement attendu rend le code illisible et coûte cher en performance.
You've already met it without understanding it
In lesson 2, inside the Produit setter, we wrote this without dwelling on it:
public function setPrix(float $prix): void {
if ($prix < 0) {
throw new InvalidArgumentException("Negative price not allowed");
}
$this->prix = $prix;
}
That throw new InvalidArgumentException — it's time to understand what it really does. It answers a question that keeps coming back: what does a method do when it CANNOT do its job? A negative price, a balance too low, a missing file.
The wrong answer is the error code. retirer() returns false on failure. Another method returns null. A third returns -1. The caller must guess each one's convention, and above all remember to check the return value on every call. Miss one, and the program carries on with a wrong value. That's the chaos of error codes.
false or null forces nobody to look at it. Code that ignores the error compiles, runs, and silently propagates corrupted data. That's exactly the bug an exception makes impossible to ignore.
throw: stop dead with an object that tells the whole story
Throwing an exception means saying "I can't continue" and stopping the method immediately. At the throw line, the method's execution halts: the following lines don't run. PHP then walks up the call stack looking for someone to handle the problem.
And it's not just a message: an exception is an object. Like any object (lesson 1), it carries data and methods. It can describe what happened:
$e->getMessage(): the readable message you wrote.$e->getFile()and$e->getLine(): the exact file and line of thethrow.$e->getCode(): an optional numeric code.$e->getTrace(): the whole call stack that led there.
function diviser(int $a, int $b): float {
if ($b === 0) {
throw new InvalidArgumentException("Division by zero not allowed");
}
return $a / $b; // skipped if b is 0
}
If nobody catches it, the exception becomes a fatal error and the script stops. That's not a flaw: a clean, loud stop beats a wrong result that spreads. Next we see how to catch it cleanly.
try / catch: catching the problem
To handle an exception instead of crashing, you wrap the risky code in a try block, followed by a catch that receives the exception object. The control flow is precise:
- The
trycode runs normally, line by line. - If a
throwoccurs (here or in a called method), PHP skips the rest of thetryand jumps into thecatch. - The
catchreceives the exception object, handles it, then execution resumes normally after the block. - If no
throwoccurs, thecatchis simply ignored.
try {
$r = diviser(10, 0);
echo "Result: $r"; // skipped: the throw happened before
} catch (InvalidArgumentException $e) {
echo "Problem: " . $e->getMessage(); // we regain control here
}
echo "\nThe program carries on."; // runs in every case
And finally? An optional block after the catch, executed no matter what: success, caught exception, or even rethrown exception. It's the place for the cleanup you never want to forget: closing a file, releasing a database connection.
Throw and catch: run it
An account refusing an overdraft, no longer with a mute false (lesson 2), but with an exception that explains the problem. Note: the program carries on after the catch.
<?php
class Compte {
private float $solde = 0;
public function deposer(float $m): void { $this->solde += $m; }
public function retirer(float $m): void {
if ($m > $this->solde) {
throw new RuntimeException(
"Insufficient balance: short by " . ($m - $this->solde) . " €"
);
}
$this->solde -= $m;
}
public function getSolde(): float { return $this->solde; }
}
$c = new Compte();
$c->deposer(50);
try {
$c->retirer(200); // throws the exception
echo "Withdrawal done\n"; // skipped
} catch (RuntimeException $e) {
echo "Error: " . $e->getMessage() . "\n";
} finally {
echo "Balance: " . $c->getSolde() . " €\n"; // always runs
}
echo "The program carries on normally.\n";
?>
The hierarchy: from specific to general
Every exception descends from a single base class: Exception. PHP then provides a ready-made family, the SPL exceptions, that inherit from it:
InvalidArgumentException: a received argument is invalid (negative price, wrong format).RuntimeException: a problem occurring at runtime (balance too low, resource unavailable).LogicException: a logic error in the program, one you should have avoided while coding.
And this is where the inheritance from lesson 3 pays off. A catch catches a class and all its child classes: that's polymorphism. Since InvalidArgumentException inherits (indirectly) from Exception, a catch (Exception $e) also catches InvalidArgumentException.
Direct consequence: you catch from the most specific to the most general. A too-broad catch placed first would grab everything, and the more precise catch blocks below would never run.
<?php
function valider(int $age): void {
if ($age < 0) throw new InvalidArgumentException("Negative age");
if ($age > 150) throw new RuntimeException("Unrealistic age");
}
foreach ([-5, 200, 30] as $age) {
try {
valider($age);
echo "$age: valid\n";
} catch (InvalidArgumentException $e) { // most specific first
echo "$age: invalid argument (" . $e->getMessage() . ")\n";
} catch (Exception $e) { // the safety net, last
echo "$age: other error (" . $e->getMessage() . ")\n";
}
}
?>
catch (Exception $e) before catch (InvalidArgumentException $e), the first catches everything (inheritance kicks in) and the second becomes dead code. Always from specific to general.
Your own exceptions: domain meaning in the name
Reusing an SPL exception is often enough. But sometimes the name of the error should speak your domain's language. CommandeIntrouvableException tells the business problem far better than a generic RuntimeException. And creating your own is trivial: just extend Exception (inheritance from lesson 3 again).
<?php
// A business exception: its name already says it all
class CommandeIntrouvableException extends Exception {}
class Catalogue {
private array $commandes = ["CMD-1" => 49.90, "CMD-2" => 12.00];
public function montant(string $ref): float {
if (!isset($this->commandes[$ref])) {
throw new CommandeIntrouvableException("Order $ref not found");
}
return $this->commandes[$ref];
}
}
$cat = new Catalogue();
foreach (["CMD-1", "CMD-9"] as $ref) {
try {
echo "$ref: " . $cat->montant($ref) . " €\n";
} catch (CommandeIntrouvableException $e) { // we target this case precisely
echo "Business problem: " . $e->getMessage() . "\n";
}
}
?>
The decision rule: reuse an SPL as long as it describes the problem well (invalid argument, runtime state). Create your own as soon as you want to catch it in a targeted way, or when its name carries business information the caller must be able to distinguish.
if and an on-screen message. "The database is unreachable" is abnormal: there, an exception is legitimate. Throwing an exception to drive an expected flow makes the code unreadable and costs performance.
🎯 Pratique
S'entraîner (clique pour ouvrir) :
⚖️ Juge le code de l'IA
Tu demandes à l'IA de « gérer l'erreur » d'un retrait. Elle te rend ceci. Ton rôle de relecteur : l'accepter tel quel ou le rejeter, et dire pourquoi.
try {
$compte->retirer(200);
} catch (Exception $e) {
// rien : on ignore, ça repartira
}
catch (Exception) est trop large ici ; viser RuntimeException serait plus précis.🧠 Rappel libre
Sans remonter dans la leçon : pourquoi faut-il placer catch (InvalidArgumentException) AVANT catch (Exception), et quel lien cela a-t-il avec l'héritage de la leçon 3 ?
catch attrape une classe et toutes ses classes filles (polymorphisme). Comme InvalidArgumentException hérite d'Exception, un catch (Exception) attrape aussi les InvalidArgumentException. Si on le place en premier, il rafle tout et le catch spécifique en dessous devient du code mort. On va donc du plus spécifique au plus général : le filet large en dernier.🔧 Débugue le code
Symptôme : ce code devrait afficher le résultat de la division, puis Fin du programme. Mais il s'arrête net sur Uncaught InvalidArgumentException: Division par zéro interdite et Fin du programme ne s'affiche jamais. Répare-le dans l'éditeur, puis exécute.
<?php
function diviser(int $a, int $b): float {
if ($b === 0) {
throw new InvalidArgumentException("Division par zéro interdite");
}
return $a / $b;
}
echo diviser(10, 0) . "\n";
echo "Fin du programme\n";
diviser(10, 0) lève une exception que personne n'attrape. Une exception non gérée devient une erreur fatale et stoppe le script avant la ligne suivante. La correction : entourer l'appel risqué d'un try et gérer le problème dans un catch : try { echo diviser(10, 0); } catch (InvalidArgumentException $e) { echo "Erreur : " . $e->getMessage(); }. Après le bloc, l'exécution reprend et Fin du programme s'affiche enfin.catch (Exception $e) AVANT catch (InvalidArgumentException $e). Que se passe-t-il quand une InvalidArgumentException est levée ?class CommandeIntrouvableException extends Exception {}, tu la lèves dans une méthode, mais l'appelant fait catch (Exception $e) { /* vide */ }. Quels sont les deux problèmes ?Tes classes savent maintenant signaler proprement leurs échecs. Reste à les organiser dans un vrai projet : éviter les collisions de noms et les charger automatiquement. C'est le rôle des namespaces.
Leçon 7 : Namespaces →