Erreurs et exceptions
PHP a des exceptions, et elles fonctionnent comme celles de Python ou de Java. Il a aussi un mĂ©canisme plus ancien, les erreurs moteur, antĂ©rieur aux exceptions et toujours prĂ©sent. La pratique moderne consiste Ă tout faire passer par les exceptions, et ce chapitre montre dâabord ce modĂšle, puis la poignĂ©e de lignes de configuration qui ramĂšnent lâancien mĂ©canisme dans le mĂȘme circuit.
Deux hiérarchies, une racine
Tout ce qui peut ĂȘtre levĂ© avec throw et rattrapĂ© avec catch implĂ©mente Throwable, et cette interface se divise en deux familles.
Error est ce que le moteur lĂšve quand votre code est faux : TypeError pour un mauvais argument, ValueError pour un bon type avec une valeur impossible, ArgumentCountError, DivisionByZeroError, UnhandledMatchError quand un match ne trouve aucune branche. Vous ne les levez pas vous-mĂȘme, et vous les rattrapez rarement, parce quâelles signalent un bug plutĂŽt quâune situation.
Exception est la famille de vos propres erreurs. PHP en livre un petit jeu dans la SPL, dont les noms tiennent lieu de documentation : InvalidArgumentException, RuntimeException, LogicException, DomainException, OutOfRangeException, UnexpectedValueException. Ătendez lâune dâelles plutĂŽt quâException directement, et vos appelants disposent dâune famille parlante Ă rattraper.
La syntaxe ne réserve aucune surprise :
<?php
declare(strict_types=1);
function parsePort(string $raw): int
{
if (!ctype_digit($raw)) {
throw new InvalidArgumentException("Not a port: $raw");
}
return (int) $raw;
}
try {
$port = parsePort('80a');
} catch (InvalidArgumentException|ValueError $e) {
echo 'Bad input: ', $e->getMessage(), PHP_EOL;
} finally {
echo 'Done.', PHP_EOL;
}
| rattrape plusieurs types dans un mĂȘme bloc (PHP 7.1). finally sâexĂ©cute quâune exception ait Ă©tĂ© levĂ©e ou non. Rattrapez Throwable quand vous voulez vraiment tout, par exemple en tĂȘte de la boucle dâun worker.
Exceptions vĂ©rifiĂ©es et valeurs dâerreur
Une fonction PHP signale un Ă©chec en levant une exception ou en renvoyant null. Rien nâoblige lâappelant Ă gĂ©rer lâun ou lâautre, et rien dans la signature ne liste ce qui peut ĂȘtre levĂ©. Si vous venez de Java, il nây a pas de clause throws ni de vĂ©rification Ă la compilation ; un docblock @throws est une courtoisie lue par votre IDE et par PHPStan ou Psalm, pas par le moteur. Si vous venez de Go ou de Rust, il nây a ni valeur de retour dâerreur ni type Result dans le langage. Quelques bibliothĂšques en proposent un, mais le PHP idiomatique ne sâen sert pas.
Le type de retour nullable plus ?? est lâidiome du « peut-ĂȘtre » :
<?php
declare(strict_types=1);
function findUser(int $id): ?array
{
return $id === 1 ? ['name' => 'Ada'] : null;
}
$name = findUser(2)['name'] ?? 'anonymous';
echo $name, PHP_EOL; // anonymous
La rĂšgle courante est de renvoyer null quand lâabsence est normale et de lever une exception quand elle ne lâest pas. Comme throw est une expression (PHP 8.0), les deux se combinent sur une ligne :
$user = findUser($id) ?? throw new RuntimeException("No user $id");
Les exceptions personnalisées transportent des données
Une exception personnalisée est une classe, donc elle peut porter un contexte typé et offrir un constructeur nommé qui compose le message pour vous :
<?php
declare(strict_types=1);
final class InsufficientFunds extends DomainException
{
public function __construct(
public readonly int $requested,
public readonly int $available,
?Throwable $previous = null,
) {
parent::__construct(
"Requested $requested, only $available available",
previous: $previous,
);
}
public static function forWithdrawal(int $requested, int $available): self
{
return new self($requested, $available);
}
}
try {
throw InsufficientFunds::forWithdrawal(100, 40);
} catch (InsufficientFunds $e) {
echo $e->available, PHP_EOL; // 40
}
Lâargument previous sert Ă chaĂźner. Rattrapez une exception de bas niveau, enveloppez-la dans une exception qui a un sens pour votre appelant, et passez lâoriginale en previous. getPrevious() remonte la chaĂźne, et tous les loggers lâaffichent, donc rien ne se perd.
try {
$pdo->query($sql);
} catch (PDOException $e) {
throw new RuntimeException('Order lookup failed', previous: $e);
}
Lâautre mĂ©canisme
Avant lâexistence des exceptions, PHP signalait les problĂšmes en Ă©mettant une erreur dâun niveau donnĂ© (notice, warning, fatal) et, pour tout ce qui nâĂ©tait pas fatal, continuait. Lâessentiel de cette machinerie a Ă©tĂ© repliĂ© dans des exceptions Error au fil des ans : une division par zĂ©ro lĂšve, un argument du mauvais type lĂšve, un appel de mĂ©thode sur null lĂšve. Quelques situations Ă©mettent encore un warning et continuent : lire une variable non dĂ©finie, une clĂ© de tableau absente, une propriĂ©tĂ© non dĂ©finie, et toute notice de dĂ©prĂ©ciation.
<?php
declare(strict_types=1);
$config = [];
echo $config['debug']; // Warning: Undefined array key "debug"
echo 'still running', PHP_EOL;
Ce « still running » est prĂ©cisĂ©ment ce quâun dĂ©veloppeur venu dâun autre langage nâattend pas. Le remĂšde est un gestionnaire dâerreurs, installĂ© Ă lâamorçage, qui transforme chaque erreur moteur en exception, et câest ce que font tous les frameworks :
<?php
declare(strict_types=1);
error_reporting(E_ALL);
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
throw new ErrorException($message, 0, $severity, $file, $line);
});
$config = [];
echo $config['debug']; // ErrorException: Undefined array key "debug"
ErrorException est une exception intĂ©grĂ©e qui se souvient de la sĂ©vĂ©ritĂ©. Ă partir de lĂ , il nây a plus quâun chemin dâĂ©chec, et try le rattrape en entier.
Quelques rĂ©glages accompagnent ce gestionnaire. error_reporting(E_ALL) garantit que rien nâest filtrĂ©. display_errors vaut On en dĂ©veloppement et Off en production, oĂč les erreurs partent dans le journal : une exception non rattrapĂ©e sur une page publique ne doit jamais afficher une trace. set_exception_handler() reçoit tout ce qui atteint le sommet sans avoir Ă©tĂ© rattrapĂ©, et câest lĂ que vous journalisez et affichez une page dâerreur gĂ©nĂ©rique. PHP 8.5 ajoute get_error_handler() et get_exception_handler() pour quâune bibliothĂšque puisse inspecter ce qui est installĂ© avant de lâenvelopper.
Ce qui ne se rattrape pas
Les erreurs fatales terminent la requĂȘte sans quâaucun catch ne les voie : mĂ©moire Ă©puisĂ©e, max_execution_time dĂ©passĂ©, mĂȘme classe dĂ©clarĂ©e deux fois. Une erreur de syntaxe dans un fichier inclus, en revanche, est une ParseError que vous pouvez rattraper depuis PHP 7. Sâil faut rĂ©agir, register_shutdown_function() sâexĂ©cute aprĂšs lâarrĂȘt du script, et error_get_last() vous dit si lâarrĂȘt Ă©tait propre. Depuis PHP 8.5, une erreur fatale affiche une trace dâappels, et une mĂ©moire Ă©puisĂ©e en production pointe enfin vers une ligne.
LâopĂ©rateur @ et assert()
Vous croiserez @ dans du code ancien : @file_get_contents($url). Il rĂ©duit au silence tout warning Ă©mis par lâexpression. Votre gestionnaire dâerreurs est toujours appelĂ©, mais error_reporting() y renvoie un masque rĂ©duit, ce qui permet au gestionnaire de savoir que @ a Ă©tĂ© utilisĂ©. ConsidĂ©rez cet opĂ©rateur comme un signal dâalerte quand vous le croisez. Le seul usage dĂ©fendable entoure une fonction qui Ă©met un warning et renvoie false en cas dâĂ©chec, immĂ©diatement suivi dâun test sur cette valeur de retour. MĂȘme lĂ , un try autour dâune alternative qui lĂšve une exception se lit mieux.
assert() mĂ©rite aussi dâĂȘtre reconnu. Câest une vĂ©rification de dĂ©veloppement, retirĂ©e en production quand zend.assertions vaut -1 dans php.ini, la valeur recommandĂ©e pour la production. Servez-vous-en pour des invariants qui documentent une intention, jamais pour valider une entrĂ©e.
Le piĂšge
Les deux erreurs classiques sont silencieuses. La premiĂšre consiste Ă rattraper Exception dans un gestionnaire de haut niveau et Ă croire que lâon a tout rattrapĂ©, alors quâune TypeError est une Error et non une Exception, et quâelle passe donc tout droit devant ce bloc. Ă la frontiĂšre de lâapplication, câest Throwable quâil faut rattraper.
La seconde est le catch vide :
try {
$cache->delete($key);
} catch (Throwable) {
}
La variable peut ĂȘtre omise (PHP 8.0), ce qui rend le bloc honnĂȘte sur le fait quâil ignore lâexception, et il y a des cas oĂč ignorer est le bon choix. Mais un catch vide autour de quoi que ce soit dâimportant est la façon la plus sĂ»re de cacher un bug pendant un an, alors journalisez au minimum ce que vous ignorez.
Tout ce qui tourne mal doit vous parvenir sous la forme dâune exception, en un seul endroit. PHP le fait, Ă condition de le lui demander.
Ce gestionnaire, et chaque classe que vous levez, vivent dans des fichiers que PHP doit trouver. Namespaces, Composer et autoloading explique comment il les trouve.