7. Les exceptions et erreurs
Lorsqu'une méthode d'une classe rencontre une erreur irrécupérable (fichier inexistant, base de données non connectée, connexion réseau hors service), elle n'affiche pas une erreur sur une console (fichier, base de données) mais lance une exception. Les exceptions étendent la classe [\Exception]. Outre des exceptions, le fonctionnement interne de PHP lance également des erreurs dont la classe de base est la classe [\Error]. Les deux classes implémentent l'interface PHP [\Throwable].
PHP 8 a profondément changé la gestion des erreurs : de nombreuses situations qui produisaient en PHP 7 un simple avertissement (et un résultat souvent absurde) lancent désormais une erreur que l'on peut intercepter. Nous le verrons au fil des exemples.
7.1. L'arborescence des scripts
Les scripts de ce chapitre sont dans le dossier [scripts-console/exceptions] :
7.2. L'interface [\Throwable]
L'interface [\Throwable] est la suivante (cf. documentation) :
Le rôle des méthodes de l'interface est le suivant :
Méthode | Rôle |
[getMessage] | rend le message d'erreur |
[getCode] | rend le code de l'erreur (un entier) |
[getFile] | rend le nom du fichier où l'objet a été créé |
[getLine] | rend le numéro de la ligne où l'objet a été créé |
[getTrace] | rend la pile des appels de fonctions sous la forme d'un tableau |
[getTraceAsString] | rend la pile des appels de fonctions sous la forme d'une chaîne de caractères |
[getPrevious] | rend l'objet [Throwable] précédent (encapsulé), ou [null] |
[__toString] | rend une représentation de l'objet sous forme de chaîne de caractères |
On remarquera que l'interface [Throwable] étend l'interface [Stringable] rencontrée au chapitre sur les classes. Une classe ne peut pas implémenter directement l'interface [Throwable] : elle doit dériver de [\Exception] ou de [\Error].
7.3. Les exceptions et erreurs prédéfinies dans PHP 8
Le script [throwables.php] suivant affiche l'arborescence des classes prédéfinies qui implémentent l'interface [Throwable]. Nous l'utilisons pour obtenir une liste exacte, celle de PHP 8.5, plutôt que de la recopier d'une documentation. Le lecteur ne perdra rien s'il ne cherche pas à comprendre son code, qui utilise l'API de réflexion de PHP (la classe [ReflectionClass] permet d'interroger PHP sur une classe : son nom, son extension d'origine, ses interfaces...) et une fonction récursive :
Résultats :
Commentaires
- ligne 13 : la fonction [get_declared_classes] rend la liste de toutes les classes connues à cet instant, prédéfinies ou non ;
- lignes 16-18 : on ne garde que les classes prédéfinies (internal), qui implémentent l'interface [Throwable] et qui appartiennent aux extensions toujours présentes dans PHP 8.5 (ligne 9). D'autres extensions ajoutent leurs propres exceptions (par exemple [PDOException] pour l'accès aux bases de données, que nous rencontrerons plus tard) ;
- lignes 24-36 : une fonction récursive qui affiche les classes filles directes de la classe [$parent], puis les filles de chacune d'elles, etc. La fonction [get_parent_class] rend [false] pour une classe sans parent : l'opérateur [?:] transforme alors [false] en [null] ;
- ligne 29 : une fonction fléchée (arrow function, PHP 7.4), forme courte d'une fonction anonyme réduite à une expression ;
- ligne 39 : on part des classes sans parent : [Error] et [Exception].
Les résultats montrent deux familles :
- la famille [Exception] : ce sont les exceptions que lancent les bibliothèques et le code applicatif. On y trouve :
- [ErrorException], qui permet de transformer un avertissement de PHP en exception (nous l'utiliserons en fin de chapitre) ;
- [JsonException] (PHP 7.3), lancée par [json_encode] et [json_decode] avec l'option [JSON_THROW_ON_ERROR] ;
- [Random\RandomException] (PHP 8.2), lancée par les fonctions de génération de nombres aléatoires ;
- les exceptions de la bibliothèque SPL (Standard PHP Library), une collection de classes et d'interfaces destinées à résoudre des problèmes rencontrés fréquemment par les développeurs. Elles forment deux sous-familles :
- [LogicException] et ses dérivées : des erreurs de programmation, qu'une correction du code doit faire disparaître : argument invalide ([InvalidArgumentException]), valeur hors du domaine attendu ([DomainException]), méthode inexistante ([BadMethodCallException])... ;
- [RuntimeException] et ses dérivées : des erreurs qui ne peuvent être détectées qu'à l'exécution : indice hors bornes ([OutOfBoundsException]), valeur inattendue ([UnexpectedValueException]), dépassement de capacité ([OverflowException])... ;
- la famille [Error] : ce sont les erreurs lancées par le moteur de PHP lui-même. On y trouve :
- [TypeError] : une valeur n'a pas le type attendu (paramètre de fonction, attribut typé, valeur de retour, opérande d'un opérateur...). Sa dérivée [ArgumentCountError] signale un nombre d'arguments insuffisant ;
- [ValueError] (PHP 8.0) : une valeur a le bon type mais une valeur invalide (par exemple une longueur négative ou nulle) ;
- [ArithmeticError] et sa dérivée [DivisionByZeroError] : erreurs dans les opérations arithmétiques ;
- [UnhandledMatchError] (PHP 8.0) : aucune branche d'une expression [match] ne convient ;
- [AssertionError] : échec d'une assertion [assert] ;
- [CompileError] et sa dérivée [ParseError] : erreur de syntaxe dans un fichier chargé par [require] ou [include] ;
- [FiberError] (PHP 8.1) et [Random\RandomError] (PHP 8.2) : erreurs liées aux fibres et aux générateurs aléatoires, que nous n'utiliserons pas .
Les deux familles n'ont aucun lien de parenté : une clause [catch (\Exception $e)] n'intercepte pas une erreur [Error]. Seule l'interface [Throwable] les réunit.
7.4. Exemple 1
Le premier exemple [exceptions-01.php] montre à la fois un avertissement PHP et une erreur :
Commentaires
- ligne 7 : on demande à PHP de signaler toutes les erreurs. Le paramètre de la fonction [error_reporting] est le niveau d'erreurs signalées. C'est une combinaison des constantes suivantes (les principales) :
Constante | Signification |
[E_ERROR] | erreur fatale à l'exécution : le script est arrêté |
[E_WARNING] | avertissement : le script continue |
[E_PARSE] | erreur de syntaxe détectée à la compilation |
[E_NOTICE] | remarque : quelque chose qui pourrait être une erreur |
[E_DEPRECATED] | utilisation d'une fonctionnalité dépréciée, qui disparaîtra dans une version future |
[E_USER_WARNING], [E_USER_NOTICE], [E_USER_DEPRECATED] | les mêmes niveaux, déclenchés par le code avec la fonction [trigger_error] |
[E_ALL] | tous les niveaux |
- la constante [E_STRICT], qui ne correspond plus à aucun message depuis PHP 8.0, est dépréciée depuis PHP 8.4, de même que l'utilisation du niveau [E_USER_ERROR] avec [trigger_error] : pour signaler une erreur fatale, le code lance une exception ;
- ligne 8 : on demande d'afficher les erreurs sur la console. En production, on fait l'inverse : les erreurs sont écrites dans un fichier de logs et non affichées ;
- ligne 12 : on accède à un élément inexistant du tableau [$var] ;
- ligne 14 : on fait une division par zéro ;
- ligne 17 : on crée une instance de la classe [SplFixedArray] de la bibliothèque SPL. Cette classe permet de créer un tableau à bornes fixes et à indices entiers ;
- ligne 21 : on accède à un élément inexistant du tableau.
Résultats :
Commentaires des résultats
- ligne 3 : accéder à une clé inexistante d'un tableau provoque un avertissement de niveau [E_WARNING] (c'était une simple remarque [E_NOTICE] en PHP 7). Cela n'interrompt pas l'exécution du script ;
- lignes 5-8 : en PHP 7, diviser un nombre par zéro provoquait un avertissement [E_WARNING] et rendait la valeur [INF] (infini). En PHP 8, l'opérateur [/] lance une erreur de type [DivisionByZeroError]. Comme elle n'est pas interceptée, elle interrompt l'exécution du script : c'est une erreur fatale (Fatal error: Uncaught...). Le message indique la pile des appels (Stack trace) au moment de l'erreur ;
- le code qui suit la division n'est pas exécuté : l'accès à un indice inexistant du tableau [SplFixedArray] n'a pas lieu. Nous le retrouverons dans les exemples suivants.
7.5. Gérer les exceptions
Le script [exceptions-02.php] montre comment gérer les exceptions :
Commentaires
- le script est celui qui a été présenté au paragraphe précédent. Seulement maintenant on a entouré le code susceptible de provoquer des erreurs par une structure [try / catch] : si le code des lignes 11-24 provoque (lance) une exception ou une erreur, celle-ci sera gérée par la clause [catch] des lignes 25-29 ;
- ligne 25 : le paramètre de la clause [catch] est le type d'exception ou d'erreur que l'on veut gérer. En mettant comme type [\Throwable] qui est une interface, on indique qu'on veut gérer toute instance de classe implémentant l'interface [\Throwable]. Comme toutes les classes d'erreurs et d'exceptions implémentent cette interface, la clause [catch] gère ici toute erreur / exception ;
- ligne 15 : l'instruction qui déclenche l'erreur. Dès qu'il y a exception, il y a branchement sur la clause [catch]. Le code qui suit dans le [try] n'est donc pas exécuté ;
- ligne 28 : la notation [$ex::class] rend le nom de la classe de l'objet [$ex].
Résultats :
Commentaires des résultats
- ligne 3 : on retrouve l'avertissement de niveau [E_WARNING]. Un avertissement n'est pas une exception et n'est donc pas géré par la clause [catch]. Nous verrons en fin de chapitre comment transformer les avertissements en exceptions ;
- ligne 4 : on a le message d'erreur écrit dans la clause [catch]. Il s'est donc produit une exception dérivée de [\Exception] ou une erreur dérivée de [\Error]. Nous voyons ici qu'il s'agit de la classe [DivisionByZeroError]. Le script s'est terminé normalement.
7.6. Paramètres de la clause [catch]
Examinons le script [exceptions-03.php] suivant :
Commentaires
- lignes 11-41 : 4 façons différentes de gérer l'exception générée par l'utilisation d'un indice incorrect avec la classe [\SplFixedArray]. L'exécution montre que cette erreur génère une exception de type [OutOfBoundsException] (ligne 2 des résultats). Dans le cours PHP 7 (et encore en PHP 8.3), c'était une exception [RuntimeException]. Le changement n'a pas cassé le code existant puisque la classe [OutOfBoundsException] dérive de la classe [RuntimeException] ;
- ligne 15 : gère une erreur de type [\Throwable]. C'est valide puisque le type [OutOfBoundsException] dérive du type [\Exception] qui implémente l'interface [\Throwable] ;
- ligne 23 : gère une erreur de type [\Exception]. C'est valide puisque le type [OutOfBoundsException] dérive du type [\Exception] ;
- ligne 31 : gère une erreur de type [\RuntimeException]. C'est valide puisque le type [OutOfBoundsException] dérive du type [\RuntimeException]. C'est un bon choix : le code fonctionne quelle que soit la version de PHP ;
- ligne 39 : gère le type exact de l'exception générée ;
- lignes 46-81 : 5 façons différentes de gérer l'erreur générée par la fonction [intdiv] lorsqu'on passe à celle-ci un diviseur égal à 0. La fonction [intdiv(int $dividend, int $divisor): int] fait la division entière [$dividend / $divisor]. Lorsque le diviseur est nul, l'erreur [\DivisionByZeroError] est lancée ;
- ligne 47 : on intercepte toute erreur implémentant l'interface [\Throwable]. C'est valide ;
- ligne 55 : on intercepte le type exact de l'erreur : c'est la méthode à privilégier ;
- ligne 63 : on intercepte le type [\ArithmeticError]. C'est valide puisque la classe [DivisionByZeroError] étend la classe [ArithmeticError] ;
- ligne 71 : on intercepte le type [\Error]. C'est valide puisque la classe [DivisionByZeroError] dérive de la classe [Error] ;
- ligne 79 : on intercepte le type [\Exception]. C'est invalide car la classe [DivisionByZeroError] n'a aucun lien avec la classe [\Exception] .
Résultats :
Les lignes 11-15 des résultats montrent que l'erreur [DivisionByZeroError] n'a pas été interceptée par la dernière clause [catch (\Exception $ex)] : c'est une erreur fatale.
7.7. Clause [finally]
La structure [try / catch] peut avoir un troisième élément et devenir une structure [try / catch / finally]. Le code de la clause [finally] est exécuté dans les deux cas suivants :
- la clause [try] ne lance pas d'exception. Elle est alors exécutée entièrement puis l'exécution du code passe à la clause [finally] qui est exécutée entièrement ;
- la clause [try] lance une exception. Elle est alors exécutée jusqu'à l'instruction qui lance l'exception. L'exécution du code passe alors à la clause [catch] qui est exécutée entièrement. Puis l'exécution du code passe à la clause [finally] qui est exécutée entièrement .
Finalement, le code de la clause [finally] est toujours exécuté. Ce scénario est utile dans le cas suivant :
- dans le [try], le code a obtenu des ressources (fichiers, bases de données, connexions réseau, files d'attente). En général ces ressources sont coûteuses. Il faut alors les rendre (on dit le plus souvent fermer) dès que c'est possible ;
- si l'acquisition des ressources a été faite dans le [try], on mettra leur restitution dans le [finally]. Cela nous assure que dans tous les cas (erreur ou pas), les ressources acquises sont restituées au système .
Le script suivant [exceptions-04.php] nous montre le fonctionnement de la clause [finally] dans diverses situations :
Commentaires du code
- ligne 7 : [$e] est une instance de la classe prédéfinie [\Exception]. On va la lancer à différents endroits ;
- lignes 11-12 : l'exception [$e] est lancée dans le [try] avec l'instruction [throw] ;
- ligne 13 : l'exception [\Exception] est interceptée et son message d'erreur écrit sur la console ;
- lignes 15-16 : la clause [finally] écrit un message. D'après ce qui a été dit précédemment, ce message devrait être tout le temps écrit, erreur ou pas dans le [try] ;
- ligne 21 : il n'y a pas d'erreur dans le [try]. On devrait là également passer dans le [finally] ;
- ligne 31 : il y a une instruction [return] dans le [try] et pas d'erreur. On peut se demander alors si on va passer dans la clause [finally]. L'exécution montre que oui. Utilisée en-dehors d'une fonction, l'instruction [return] termine le script.
Résultats :
Examinons un autre cas [exceptions-05.php] :
Commentaires
- ligne 9 : l'instruction [exit] arrête immédiatement l'exécution du script : la clause [finally] n'est pas exécutée ;
- lignes 7-12 : un exemple de [try / finally] sans clause [catch]. C'est possible.
Résultats :
7.8. Créer ses propres classes d'exceptions
Dans un projet un peu important, il est utile de différencier les différentes erreurs en les encapsulant dans différentes classes d'exceptions. Dans les scripts précédents, nous avons vu que toute exception pouvait être interceptée par une clause [catch (\Throwable $ex)]. C'est recommandé si on n'a aucune idée de l'erreur interceptée et que le traitement est le même pour toutes les erreurs. C'est parfois le cas mais on a souvent besoin d'adapter le traitement au type exact de l'erreur. Il faut alors différencier les erreurs entre elles.
Examinons le script [exceptions-06.php] suivant :
Commentaires
- lignes 7-9 : on définit deux classes [Exception1] et [Exception2] toutes deux dérivées de la classe prédéfinie [\RuntimeException]. Le corps de ces classes est vide. Autrement dit, on ne les utilise que pour leurs types : c'est parce qu'elles ont des types différents qu'on va pouvoir différencier ces deux exceptions dans les clauses [catch] ;
- lignes 16-18 : on définit deux variables [$e1] et [$e2] ayant respectivement les types [Exception1] et [Exception2]. Le constructeur utilisé est celui de la classe [Exception] dont la signature est [__construct(string $message = "", int $code = 0, ?Throwable $previous = null)]. Pour [$e2], on a précisé un code d'erreur ;
- lignes 21-31 : on a une structure [try / catch / catch]. Cela permet de gérer différents types d'exception avec différentes clauses [catch]. La première clause dont le type convient est exécutée, les autres sont ignorées ;
- ligne 25 : intercepte les exceptions de type [Exception1] ;
- ligne 28 : intercepte les exceptions de type [Exception2] ;
- ligne 51 : intercepte les exceptions de type [Exception1] ou ([|]) [Exception2]. Cette syntaxe existe depuis PHP 7.1 ;
- ligne 62 : le nom de la classe de l'exception interceptée ;
- lignes 64-67 : les informations portées par l'exception, obtenues par les méthodes de l'interface [Throwable]. La fonction [basename] rend le nom d'un fichier sans le chemin de son dossier.
Résultats :
Commentaires des résultats
- lignes 2-18 : le « contenu » d'une exception, affiché par [var_dump] :
- lignes 3-4 : le message d'erreur ;
- lignes 7-8 : le code d'erreur ;
- lignes 9-10 : le nom du fichier dans lequel l'exception a été créée. La longueur indiquée (string(76)) est celle du chemin complet du fichier : elle dépend du dossier dans lequel se trouve le script ;
- lignes 11-12 : la ligne à laquelle l'exception a été créée ;
- lignes 13-15 : la pile des appels de fonctions au moment de la création de l'exception. Elle est vide ici car l'exception a été créée dans le script principal et non dans une fonction ;
- lignes 16-17 : l'exception précédente. Une exception peut encapsuler une autre exception et définir ainsi une pile d'exceptions. L'attribut [previous] va permettre d'exploiter cette pile ;
- lignes 29-33 : on remarquera que le fichier et la ligne de l'exception sont ceux de sa création par [new] (ligne 18) et non ceux de son lancement par [throw].
7.9. Relancer une exception
Une exception peut être lancée plusieurs fois comme le montre le script [exceptions-07.php] suivant :
Commentaires
- ligne 9 : on lance une exception ;
- ligne 10 : on l'intercepte ;
- ligne 12 : on la relance. Elle passe alors dans la structure [try / catch / finally] du niveau supérieur. Ce scénario est courant : un code intercepte une exception pour faire un traitement local (écrire un log, annuler une transaction...) puis la relance pour que le code appelant soit lui aussi prévenu ;
- ligne 17 : on l'intercepte de nouveau ;
- lignes 13-15 : l'exécution montre que même après le [throw] de la ligne 12, on passe bien dans la clause [finally] du [try / catch / finally] interne.
Résultats :
7.10. Exploitation d'une pile d'exceptions
Une exception peut encapsuler une autre exception qui elle-même peut en encapsuler une autre formant finalement une pile d'exceptions. Voici un exemple [exceptions-08.php] :
Commentaires
- lignes 7-15 : définissent trois classes d'exceptions dérivées de l'exception prédéfinie [RuntimeException] ;
- ligne 20 : une instance de la classe [Exception3] est encapsulée dans une instance de la classe [Exception2] qui elle-même est encapsulée dans une instance de la classe [Exception1]. Le constructeur utilisé ici est le constructeur de la classe [Exception] : [__construct(string $message = "", int $code = 0, ?Throwable $previous = null)]. Le 3e paramètre du constructeur permet d'encapsuler une exception. Cela peut être utile dans le scénario suivant :
- on définit une méthode M qui peut générer une exception de type [Exception1] et seulement de ce type, pour des raisons de compatibilité par exemple avec une interface ;
- or dans la méthode M peuvent se produire d'autres types d'exceptions. Pour remonter une erreur au code appelant la méthode M, on encapsulera alors ces exceptions dans le type [Exception1] que l'on lancera. Cela permet de ne pas perdre l'information contenue dans l'exception encapsulée et qui a été la cause originelle de l'erreur ;
- lignes 24-29 : montrent comment parcourir la pile des exceptions internes à une exception. La méthode [getPrevious] rend l'exception encapsulée ou [null] s'il n'y en a pas. La boucle [do ... while] exécute son corps au moins une fois puis tant que la condition est vraie.
Résultats :
- lignes 2-50 : l'exception [$e1] contient dans son attribut [previous] l'exception [Exception2] (lignes 17-49) qui elle-même contient l'exception [Exception3] (lignes 32-48) ;
- lignes 51-53 : le parcours de la pile des exceptions.
7.11. Des avertissements de PHP 7 devenus des erreurs en PHP 8
Nous avons vu que la division par zéro, qui produisait un avertissement en PHP 7, lance une erreur en PHP 8. Ce n'est pas un cas isolé : PHP 8 a transformé en erreurs de nombreux avertissements dont l'exécution continuait avec un résultat faux. Le script [exceptions-09.php] en présente quelques-uns :
Résultats :
Commentaires
- ligne 19 : l'utilisation d'une variable non définie reste un simple avertissement, mais de niveau [E_WARNING] (c'était une remarque [E_NOTICE] en PHP 7). La variable vaut [null] et le script continue (lignes 2-4 des résultats) ;
- ligne 22 : le modulo par zéro lance une erreur [DivisionByZeroError] (ligne 5). C'était déjà le cas en PHP 7 : seule la division [/] par zéro y produisait un simple avertissement ;
- ligne 28 : une opération arithmétique sur un tableau provoque une erreur [TypeError] (ligne 6) ;
- ligne 34 : en PHP 7, une opération arithmétique sur une chaîne non numérique provoquait l'avertissement « A non-numeric value encountered » et la chaîne valait 0. En PHP 8, c'est une erreur [TypeError] (ligne 7). Une chaîne numérique (["12" * 2]) reste acceptée ;
- ligne 40 : en PHP 7, passer un paramètre d'un mauvais type à une fonction prédéfinie provoquait un avertissement et la fonction rendait [null]. En PHP 8, toutes les fonctions prédéfinies lancent une erreur [TypeError], comme les fonctions écrites par le développeur (ligne 8). Le message désigne le paramètre par son numéro et son nom ;
- ligne 46 : le paramètre a le bon type ([int]) mais une valeur invalide (une longueur doit être positive) : c'est une erreur [ValueError], nouvelle en PHP 8.0 (ligne 9). En PHP 7, c'était un avertissement ;
- ligne 52 : la fonction [somme] attend deux arguments et n'en reçoit qu'un : erreur [ArgumentCountError] (ligne 10) ;
- lignes 59-62 : l'expression [match] n'a pas de branche pour la valeur 8 et n'a pas de branche [default] : erreur [UnhandledMatchError] (ligne 11). C'est une différence importante avec l'instruction [switch] qui, elle, ne fait rien lorsqu'aucun [case] ne convient ;
- ligne 69 : l'appel d'une méthode sur [null] est une erreur [Error] (ligne 12). C'était déjà le cas en PHP 7 ;
- ligne 74 : l'opérateur nullsafe [?->] (PHP 8.0) rend [null] si l'objet est [null], au lieu d'appeler la méthode. On évite ainsi d'écrire un test [if ($objet !== null)] (ligne 13).
Ces changements rendent les programmes PHP 8 plus sûrs : une erreur interrompt le traitement au lieu de le laisser continuer avec des données fausses. Ils obligent aussi le développeur à prévoir les clauses [catch] correspondantes.
7.12. [catch] multiple, [catch] sans variable et [throw] expression
Le script [exceptions-10.php] présente trois facilités de PHP 8 et un usage courant de l'encapsulation d'exceptions :
Résultats :
Commentaires
- ligne 9 : depuis PHP 8.0, [throw] est une expression et non plus seulement une instruction. On peut donc l'utiliser là où une valeur est attendue : ici, après l'opérateur [??]. Si la clé [age] n'existe pas, l'exception est lancée ; sinon [$age] reçoit sa valeur ;
- ligne 10 : [throw] utilisé dans une expression conditionnelle [? :] ;
- ligne 14 : une fonction fléchée dont le corps se réduit à lancer une exception. Avant PHP 8.0, c'était impossible : une fonction fléchée ne peut contenir qu'une expression ;
- ligne 21 : une seule clause [catch] pour deux types d'exceptions (lignes 3-4 des résultats). On remarquera que [InvalidArgumentException] et [DomainException] dérivent toutes deux de [LogicException] : un [catch (\LogicException $ex)] aurait eu le même effet ;
- ligne 29 : depuis PHP 8.0, la variable de la clause [catch] est facultative lorsqu'on n'a pas besoin de l'exception, seulement de son type (ligne 5) ;
- lignes 35-37 : une exception propre à l'application. Dans les chapitres suivants, nous écrirons des couches d'accès aux données (DAO, Data Access Object) qui ne lanceront que ce type d'exception, quelle que soit l'origine de l'erreur (fichier, jSON, base de données) ;
- lignes 39-47 : la fonction [lireConfiguration] décode une chaîne jSON. L'exception de bas niveau [JsonException] est interceptée et encapsulée (ligne 45) dans une exception [DaoException]. La cause originelle est passée en 3e paramètre du constructeur ;
- ligne 53 : le code appelant ne connaît que [DaoException], mais il peut retrouver la cause originelle de l'erreur avec la méthode [getPrevious] (lignes 6-7 des résultats).
7.13. Transformer les avertissements en exceptions : [ErrorException]
Malgré les progrès de PHP 8, certaines anomalies restent de simples avertissements : clé de tableau inexistante, variable non définie, fichier impossible à ouvrir avec [fopen]... Ces avertissements ne peuvent être interceptés par un [try / catch]. On peut cependant installer un gestionnaire d'erreurs qui les transforme en exceptions [ErrorException]. C'est ce que fait le script [exceptions-11.php] :
Résultats :
Commentaires
- lignes 7-9 : la fonction [set_error_handler] installe un gestionnaire d'erreurs : une fonction que PHP appellera à chaque avertissement, remarque ou dépréciation, au lieu d'afficher son message. Elle reçoit le niveau de l'erreur, son message, le fichier et la ligne où elle s'est produite. Ici, elle lance une exception [ErrorException] qui mémorise ces informations. Le type de retour [bool] est celui que prévoit la documentation de [set_error_handler] : la fonction rendrait [false] pour laisser PHP traiter l'erreur normalement. Elle ne rend jamais rien ici puisqu'elle lance toujours une exception ;
- ligne 14 : la clé [prenom] n'existe pas. Au lieu d'un avertissement, on obtient une exception [ErrorException] que l'on intercepte (ligne 2 des résultats) ;
- ligne 17 : la méthode [getSeverity], propre à la classe [ErrorException], rend le niveau de l'erreur d'origine (ligne 3) ;
- ligne 21 : l'ouverture d'un fichier inexistant devient également une exception (ligne 4) ;
- ligne 26 : rétablit le gestionnaire d'erreurs précédent, ici celui de PHP.
Cette technique est utilisée par de nombreux frameworks PHP (Symfony, Laravel) : en développement, tout avertissement devient une exception, ce qui oblige à corriger le code. L'outil de tests [PHPUnit] que nous utiliserons plus loin procède de façon analogue : il peut faire échouer un test qui produit un avertissement ou une dépréciation.