Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Un arbre avec Throwable à la racine qui se sépare en deux branches : Error, dont les feuilles sont TypeError, ValueError et UnhandledMatchError avec une petite icÎne d'engrenage, et Exception, dont les feuilles sont RuntimeException, InvalidArgumentException et une feuille étiquetée « les vÎtres »

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"
De petits papiers étiquetés warning, notice et deprecated tombent d'en haut dans un entonnoir. De son bec sort une seule enveloppe propre, tamponnée du mot exception, qui atterrit dans un bloc try dessiné comme une boßte

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.