Déclenchement, capture et gestion des exceptions PHP : try, throw, catch

Dans cette nouvelle leçon, nous allons présenter une nouvelle façon de gérer les erreurs qui va s’avérer souvent meilleure que de simplement créer et passer un gestionnaire d’erreur à la fonction set_error_handler().

On va ainsi apprendre à gérer les erreurs en manipulant des objets qu’on appelle des exceptions et qui vont être crées et « lancés » dès qu’une erreur définie va survenir.

 

Présentation des exceptions en PHP 5

La version 5 de PHP a introduit une nouvelle façon de gérer la plupart des erreurs en utilisant de qu’on appelle des exceptions. Cette nouvelle façon de procéder se base sur le PHP orienté objet et sur la classe Exception.

L’idée ici va être de créer ou de « lancer » un nouvel objet Exception lorsqu’une erreur spécifique est détectée. Dès qu’une exception est lancée, le script va suspendre son exécution et le PHP va chercher un endroit dans le script où l’exception va être « attrapée ».

Utiliser des exceptions va nous permettre de gérer les erreurs de manière plus fluide et de personnaliser la façon dont un script doit gérer certaines erreurs.

Notez que quasiment tous les langages serveurs utilisent le concept d’exceptions pour prendre en charge les erreurs car c’est la meilleure façon de procéder à ce jour.

 

Lancer et attraper une exception

En pratique, la gestion d’une erreur via une exception va se faire en trois temps :

  1. On va définir quand une exception doit être lancée avec une instruction throw ;
  2. On va créer un bloc catch dont le but va être d’attraper l’exception si celle-ci a été lancée et de définir la façon dont doit être gérée l’erreur ;
  3. On va utiliser un bloc try dans lequel le code qui peut potentiellement retourner une erreur va être exécuté.

Illustrons immédiatement tout cela avec un exemple concret. Pour cela, imaginons que l’on définisse une fonction qui divise deux nombres non connus à l’avance entre eux. Comme vous le savez, on ne peut pas diviser un nombre par zéro. Si on tente de le faire, une erreur sera renvoyée par le PHP. Nous allons justement lancer une exception pour gérer ce cas particulier.

Commençons déjà par créer notre script sans aucune prise en charge des erreurs :

<!DOCTYPE html>
<html>
    <head>
        <title>Cours PHP & MySQL</title>
        <meta charset="utf-8">
        <link rel="stylesheet" href="cours.css">
    </head>
    
    <body>
        <h1>Titre principal</h1>
        <form action='cours.php' method='post'>
            <label for='n1'>Numérateur :</label>
            <input type='number' id='n1' name='n1'><br><br>
            <label for='n2'>Dénominateur :</label>
            <input type='number' id='n2' name='n2'><br>
            <input type='submit' value='Envoyer'><br><br>
        </form>
        <?php
            function division($x, $y){
                echo '<br>Résultat de' .$x. '/' .$y. ' : ' .($x / $y);
            }
            
            //En pratique, il faudrait vérifier les données envoyées
            if(isset($_POST['n1']) && isset($_POST['n2'])){
                division($_POST['n1'],$_POST['n2']);
            }

        ?>
        <p>Un paragraphe</p>
    </body>
</html>

 

Une erreur est déclenchée sans prise en charge en PHP

Ici, on définit une fonction division() dont le rôle est de diviser un nombre par un autre et de renvoyer le résultat. Les nombres seront passés par nos utilisateurs via un formulaire. Évidemment, en pratique, il faudra vérifier les données envoyées avant de les manipuler mais ce n’est pas l’objet de la leçon.

Ici, on se contente simplement de s’assurer que des données ont bien été envoyées et si c’est le cas on exécute division(). Évidemment, si on renseigne 0 comme dénominateur, une erreur est renvoyée puisqu’on n’a pas le droit de diviser par zéro.

Pour gérer cette erreur, on pourrait tout à fait utiliser un gestionnaire d’erreurs et la fonction set_error_handler() comme ceci :

<!DOCTYPE html>
<html>
    <head>
        <title>Cours PHP & MySQL</title>
        <meta charset="utf-8">
        <link rel="stylesheet" href="cours.css">
    </head>
    
    <body>
        <h1>Titre principal</h1>
        <form action='cours.php' method='post'>
            <label for='n1'>Numérateur :</label>
            <input type='number' id='n1' name='n1'><br><br>
            <label for='n2'>Dénominateur :</label>
            <input type='number' id='n2' name='n2'><br>
            <input type='submit' value='Envoyer'><br><br>
        </form>
        <?php
            function division($x, $y){
                echo '<br>Résultat de' .$x. '/' .$y. ' : ' .($x / $y);
            }
            
            set_error_handler(function($niveau, $message, $fichier, $ligne){
                echo 'Erreur : ' .$message. '<br>';
                echo 'Niveau de l\'erreur : ' .$niveau. '<br>';
                echo 'Erreur dans le fichier : ' .$fichier. '<br>';
                echo 'Emplacement de l\'erreur : ' .$ligne. '<br>';
            });
            
            //En pratique, il faudrait vérifier les données envoyées
            if(isset($_POST['n1']) && isset($_POST['n2'])){
                division($_POST['n1'],$_POST['n2']);
            }
        ?>
        <p>Un paragraphe</p>
    </body>
</html>

 

On utilise une fonction gestionnaire d'erreur en PHP

Cela fonctionne bien ici. Cependant, vous devez savoir que dans un script plus complexe, il va être difficile de s’assurer que la fonction set_error_handler() soit appelée avec les bonnes valeurs et au bon moment. Pour ces raisons, on préfèrera souvent utiliser les exceptions pour gérer les erreurs. Regardez à nouveau le code :

<!DOCTYPE html>
<html>
    <head>
        <title>Cours PHP & MySQL</title>
        <meta charset="utf-8">
        <link rel="stylesheet" href="cours.css">
    </head>
    
    <body>
        <h1>Titre principal</h1>
        <form action='cours.php' method='post'>
            <label for='n1'>Numérateur :</label>
            <input type='number' id='n1' name='n1'><br><br>
            <label for='n2'>Dénominateur :</label>
            <input type='number' id='n2' name='n2'><br>
            <input type='submit' value='Envoyer'><br><br>
        </form>
        <?php
            function division($x, $y){
                if($y == 0){
                    throw new Exception('Division par zéro impossible', 15);
                }
                else{
                    echo '<br>Résultat de' .$x. '/' .$y. ' : ' .($x / $y);
                }
            }
            
            //En pratique, il faudrait vérifier les données envoyées
            if(isset($_POST['n1']) && isset($_POST['n2'])){
                try{
                    division($_POST['n1'],$_POST['n2']);
                }
                catch(Exception $e){
                    echo 'Message d\'erreur : ' .$e->getMessage();
                    echo '<br>';
                    echo 'Code d\'erreur : ' .$e->getCode();
                    echo '<br>';
                    echo $e->getFile();
                }
            } 
        ?>
        <p>Un paragraphe</p>
    </body>
</html>

 

On utilise une structure try throw catch pour lancer et gérer une exception en PHP

Dans l’exemple ci-dessus, on utilise cette fois-ci les exceptions pour prendre en charge les erreurs. L’idée derrière les exceptions va être d’anticiper les situations problématiques (situations qui vont pouvoir causer une erreur) et de lancer une exception si la situation est rencontrée.

C’est ce qu’on fait ici dans le code de notre fonction division() : on a identifié que le PHP renverrait une erreur dans le cas où un utilisateur tenterait une division par zéro. Dans ce cas là, on va lancer une exception. Pour cela, on utilise la syntaxe throw new Exception qui lance un objet de la classe Exception.

La classe Exception possède un constructeur qui va pouvoir accepter un message d’erreur et un code d’erreur personnalisé de notre choix. Ici, je passe le message « Division par zéro impossible » et je choisis le code « 15 ».

Ce code va être très utile dans le cas d’un « vrai » site si on souhaite effectuer un suivi et un rapport des erreurs survenues puisqu’on va ainsi pouvoir identifier très rapidement et précisément les erreurs.

Dès qu’une exception est lancée, il va falloir l’attraper. On va faire cela dans un deuxième bloc catch. Le but de ce bloc va déjà être de capturer une exception si une exception a été lancée. Ici, si c’est le cas, on place les informations liées à l’exception dans $e (le nom $e est choisi arbitrairement, vous pouvez choisir le nom de votre choix).

Au sein du bloc catch, nous allons préciser les actions à mener en cas d’exception. Ici, on se contente de renvoyer notre message d’erreur, notre code d’erreur et le fichier dans lequel l’exception a été lancée.

Pour comprendre ce code, vous devez savoir que $e est un objet de la classe Exception et que la classe Exception possède des méthodes dont notamment :

  • getMessage() qui va renvoyer le message d’erreur défini lors du lancement de l’exception ;
  • getCode() qui va renvoyer le code d’erreur défini lors du lancement de l’exception ;
  • getFile() qui va renvoyer le chemin du fichier depuis lequel l’exception a été lancée ;
  • getLine() qui va renvoyer la ligne du fichier depuis lequel l’exception a été lancée.

Finalement, on va exécuter le code potentiellement problématique dans un bloc try (bloc « d’essai »). Si aucune erreur n’est rencontrée, alors aucune exception ne sera lancée et le code s’exécutera normalement. Si une erreur est rencontrée, alors une exception sera lancée et attrapée dans le bloc catch dans lequel on va décider de la marche à suivre.

 

Les exceptions et les erreurs en PHP 7

L’une des grandes limites du PHP 5 est qu’il était quasiment impossible de gérer des erreurs fatales. En effet, une erreur fatale n’appelait pas le gestionnaire d’erreurs défini dans set_error_handler() et il était également impossible de lancer une exception en cas d’erreur fatale.

Ainsi, en PHP 5, dès qu’une erreur fatale était rencontrée, le script s’arrêtait brutalement. Certains développeurs disent qu’il s’agit là d’un comportement normal puisqu’une erreur fatale ne devrait pas pouvoir être gérée d’une autre façon / ignorée. Ce point de vue est parfois vrai mais pas toujours justifié en fonction de l’erreur fatale.

La version 7 du PHP modifié cela et nous offre désormais un moyen de gérer certaines erreurs fatales via le lancement d’exceptions. La difficulté ici est que pour des raisons de rétrocompatibilité du code (cohérence entre différentes versions de PHP qui coexistent), les exceptions lancées par les erreurs fatales ne vont pas être des instances de la classe Exception mais d’une nouvelle classe créée en PHP 7 pour cela : la classe Error qui est très similaire dans sa construction à la classe Exception.

Note : A ce point du cours, vous n’êtes plus tout à fait débutants et il y a donc une notion que vous devriez intégrer et qui va grandement faciliter la compréhension des différents langages de programmation : les langages de programmation ne sont JAMAIS parfaits. Certains langages prennent des directions de développement puis reviennent sur leur pas ou intègrent de nouveaux composants au fil du temps. Cela fait que chaque langage dispose d’un héritage de composants désuets et que parfois il est difficile de maintenir un ensemble qui fasse du sens.

C’est le cas du PHP qui est un langage très performant mais cependant également très loin d’être parfait d’un point de vue sémantique et qui possède encore certains comportements qui ne font tout simplement aucun sens. Vous devez absolument garder cela en tête et essayer d’utiliser les langages au mieux tout en gardant à l’esprit que la perfection n’existe pas !

Retour à nos erreurs. Une nouvelle classe a donc été créée en PHP 7 pour utiliser les exceptions avec certains types d’erreurs. Pour apporter un peu de cohérence dans la gestion des erreurs et tenter une unification, une interface a également été créée : l’interface Throwable.

Cette interface est en PHP 7 l’interface de base pour tout objet qui peut être jeté ou lancé grâce à la déclaration throw et va être implémentée à la fois par la classe Error et par la classe Exception.

On va ainsi pouvoir utiliser l’interface Throwable pour attraper à la fois des exceptions issues de la classe Exception et des exceptions issues de la classe Error.

Cependant, comme Throwable est une interface, on ne va pas pouvoir l’instancier directement.

En PHP 7, lorsqu’une erreur fatale et récupérable (erreur fatale de type « recoverable ») survient, une exception de la classe Error est automatiquement lancée. On va ainsi pouvoir l’attraper dans un bloc catch comme nos exceptions « classiques » à part que cette fois-ci le bloc catch devra utiliser la syntaxe catch(Error $e){} ou éventuellement plus généralement catch(Throwable $t){} qui va permettre d’attraper les exceptions issues de la classe Error et celles de la classe Exception.

Notez qu’il est généralement considéré comme une bonne pratique d’utiliser les classes les plus précises pour attraper les exceptions et les gérer de la meilleure façon.

Regardez l’exemple ci-dessous pour bien comprendre :

<!DOCTYPE html>
<html>
    <head>
        <title>Cours PHP & MySQL</title>
        <meta charset="utf-8">
        <link rel="stylesheet" href="cours.css">
    </head>
    
    <body>
        <h1>Titre principal</h1>
        <?php
            try{
                bonjour();
            }
            catch(Error $e){
               echo $e->getMessage();
            }
            
            echo '<br><br>';
            
            try{
                if(!function_exists('test')){
                    throw new Error('La fonction n\'est pas définie');
                }
            }
            catch(Error $e){
               echo $e->getMessage();
            }
        ?>
        <p>Un paragraphe</p>
    </body>
</html>

 

On utilise une structure try throw catch pour lancer et gérer une erreur en PHP

Ici, on commence par essayer d’appeler une fonction bonjour() qui n’est pas définie. Vous devez savoir que lorsqu’on tente d’exécuter une fonction non définie, le PHP lève une erreur fatale. En PHP 7, une exception de la classe Error est également lancée. On capture cette erreur dans le bloc catch juste en dessous et on affiche les informations relatives à l’erreur avec la méthode getMessage() de la classe Error.

On va ensuite gérer une autre erreur du même type en lançant nous-même l’exception cette fois-ci pour personnaliser le message par exemple. Pour cela, on utilise la fonction function_exists() qui vérifie si une fonction a été définie et renvoie true si c’est le cas ou false dans le cas contraire.

Ici, on demande à function_exists() de vérifier si une fonction test() a été définie. Ce n’est pas le cas ; notre fonction va donc renvoyer false et on va lancer une exception issue de la classe Error en précisant un message personnalisé.

Ici, notez bien qu’on rentre dans le if lorsque function_exists() renvoie false puisqu’on a utilisé l’opérateur ! pour inverser la valeur logique de notre test.

Ensuite, on utilise un bloc catch pour capturer l’exception et renvoyer le message personnalisé.

Ici, nous ne faisons qu’echo les informations relatives à l’erreur, ce qui n’est pas très utile en soi. Cependant, vous allez pouvoir définir toute sorte de code dans votre bloc catch pour dicter le comportement du script en cas d’exception.

 

Le bloc finally

Depuis PHP 5.5, on va pouvoir spécifier un bloc finally à la suite des blocs catch.

L’intérêt principal de ce bloc est que le code contenu dans finally sera toujours exécuté à la suite des blocs try et catch et avant la reprise de l’exécution normale du script, et ceci qu’une exception ait été lancée ou pas.

On va donc vouloir utiliser ce bloc lorsqu’on souhaite absolument qu’un code s’exécute quelle que soit la situation, comme par exemple dans le cas où on voudrait fermer une connexion à une base de données.

La grande différence entre un code dans un bloc finally et un code dans l’espace global du script (en dehors des blocs cités précédemment) est qu’un code suivant les blocs try et finally et en dehors de ceux-ci ne sera pas exécuté dans le cas où une exception a été lancée mais pas attrapée (car dans ce cas une erreur fatale sera levée).

Regardez plutôt l’exemple ci-dessous :

<!DOCTYPE html>
<html>
    <head>
        <title>Cours PHP & MySQL</title>
        <meta charset="utf-8">
        <link rel="stylesheet" href="cours.css">
    </head>
    
    <body>
        <h1>Titre principal</h1>
        <?php
            try{
                bonjour();
            }
            finally{
                echo 'Toujours affiché';
            }
            echo 'Non affiché ici car exception lancée et non capturée';
        ?>
        <p>Un paragraphe</p>
    </body>
</html>

 

Ajout d'un bloc finally dans la gestion des exceptions en PHP qui s'exécutera dans tous les cas

Laisser un commentaire