Mis à jour le lundi 8 janvier 2018
  • 30 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Les exceptions

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Actuellement, vous connaissez les erreurs fatales, les alertes, les erreurs d'analyse ou encore les notices. Nous allons découvrir dans ce chapitre une façon différente de gérer les erreurs. Nous allons, en quelque sorte, créer nos propres types d'erreurs. Les exceptions sont des erreurs assez différentes qui ne fonctionnent pas de la même manière. Comme vous le verrez, cette nouvelle façon de gérer ses erreurs est assez pratique. Par exemple, vous pouvez attraper l'erreur pour l'afficher comme vous voulez plutôt que d'avoir un fameux et plutôt laid Warning.

Cela ne s'arrêtera pas là. Puisque la gestion d'erreurs est assez importante sur un site, je dédie une partie de ce chapitre au moyen de gérer ses erreurs facilement et proprement, que ce soit les erreurs que vous connaissez déjà ou les exceptions.

Une différente gestion des erreurs

Les exceptions, comme nous l'avons évoqué dans l'introduction de ce chapitre, sont une façon différente de gérer les erreurs. Celles-ci sont en fait des erreurs lancées par PHP lorsque quelque chose qui ne va pas est survenu. Nous allons commencer par lancer nos propres exceptions. Pour cela, on va devoir s'intéresser à la classeException.

Lancer une exception

Une exception peut être lancée depuis n'importe où dans le code. Quand on lance une exception, on doit, en gros, lancer une instance de la classeException. Cet objet lancé contiendra le message d'erreur ainsi que son code. Pensez à spécifier au moins le message d'erreur, bien que celui-ci soit facultatif. Je ne vois personnellement pas l'intérêt de lancer une exception sans spécifier l'erreur rencontrée. Pour le code d'erreur, il n'est pas (pour l'instant) très utile. Libre à vous de le spécifier ou non. Le troisième et dernier argument est l'exception précédente. Là aussi, spécifiez-là si vous le souhaitez, mais ce n'est pas indispensable.

Passons à l'acte. Nous allons créer une simple fonction qui aura pour rôle d'additionner un nombre avec un autre. Si l'un des deux nombres n'est pas numérique, alors on lancera une exception de typeExceptionà l'aide du motthrow(= lancer). On va donc lancer une nouvelle Exception. Le constructeur de la classeExceptiondemande en paramètre le message d'erreur, son code et l'exception précédente. Ces trois paramètres sont facultatifs.

<?php
function additionner($a, $b)
{
  if (!is_numeric($a) || !is_numeric($b))
  {
    // On lance une nouvelle exception grâce à throw et on instancie directement un objet de la classe Exception.
    throw new Exception('Les deux paramètres doivent être des nombres');
  }
  
  return $a + $b;
}

echo additionner(12, 3), '<br />';
echo additionner('azerty', 54), '<br />';
echo additionner(4, 8);

Et là, vous avez :

Résultat affiché par le script
Résultat affiché par le script

Et voilà votre première exception qui apparaît, d'une façon assez désagréable, devant vos yeux ébahis. Décortiquons ce que PHP veut nous dire.

Premièrement, il génère une erreur fatale. Et oui, une exception non attrapée génère automatiquement une erreur fatale. Nous verrons plus tard ce que signifie « attraper ».

Deuxièmement, il nous dit Uncaught exception 'Exception' with message 'Les deux paramètres doivent être des nombres' ce qui signifie « Exception 'Exception' non attrapée avec le message 'Les deux paramètres doivent être des nombres' ». Ce passage se passera de commentaire, la traduction parle d'elle-même : on n'a pas attrapé l'exception 'Exception' (= le nom de la classe instanciée pour créer l'objet qui a été lancé) avec tel message (ici, c'est le message spécifié dans le constructeur).

Et pour finir, PHP nous dit où a été lancée l'exception, depuis quelle fonction, à quelle ligne, etc.

Maintenant, puisque PHP n'a pas l'air content que l'on n'ait pas « attrapé » cette exception, et bien c'est ce que nous allons faire.

Attraper une exception

Afin d'attraper une exception, il faut d'abord qu'elle soit lancée. Le problème, c'est qu'on ne peut pas dire à PHP que toutes les exceptions lancées doivent être attrapées : c'est à nous de lui dire que l'on va essayer d'effectuer telle ou telle instruction et, si une exception est lancée, alors on attrapera celle-ci afin qu'aucune erreur fatale ne soit lancée et que de tels messages ne s'affichent plus.

Nous allons dès à présent placer nos instructions dans un bloctry. Celles-ci seront à placer entre une paire d'accolades. Qui dit bloctrydit aussi bloccatchcar l'un ne va pas sans l'autre (si vous mettez l'un sans l'autre, une erreur d'analyse sera levée). Ce bloccatcha une petite particularité. Au lieu de placercatchsuivi directement des deux accolades, nous allons devoir spécifier, entre une paire de parenthèses placée entrecatchet l'accolade ouvrante, le type d'exception à attraper suivi d'une variable qui représentera cette exception. C'est à partir d'elle que l'on pourra récupérer le message ou le code d'erreur grâce aux méthodes de la classe.

Commençons en douceur en attrapant simplement toute exceptionException:

<?php
function additionner($a, $b)
{
  if (!is_numeric($a) || !is_numeric($b))
  {
    throw new Exception('Les deux paramètres doivent être des nombres'); // On lance une nouvelle exception si l'un des deux paramètres n'est pas un nombre.
  }
  
  return $a + $b;
}

try // On va essayer d'effectuer les instructions situées dans ce bloc.
{
  echo additionner(12, 3), '<br />';
  echo additionner('azerty', 54), '<br />';
  echo additionner(4, 8);
}

catch (Exception $e) // On va attraper les exceptions "Exception" s'il y en a une qui est levée.
{

}

Et là, miracle, vous n'avez plus que 15 qui s'affiche, et plus d'erreur ! Par contre, les deux autres résultats ne sont pas affichés, et il serait intéressant de savoir pourquoi. Nous allons afficher le message d'erreur. Pour ce faire, il faut appeler la méthodegetMessage(). Si vous souhaitez récupérer le code d'erreur, il faut appelergetCode().

<?php
function additionner($a, $b)
{
  if (!is_numeric($a) || !is_numeric($b))
  {
    throw new Exception('Les deux paramètres doivent être des nombres');
  }
  
  return $a + $b;
}

try // Nous allons essayer d'effectuer les instructions situées dans ce bloc.
{
  echo additionner(12, 3), '<br />';
  echo additionner('azerty', 54), '<br />';
  echo additionner(4, 8);
}

catch (Exception $e) // On va attraper les exceptions "Exception" s'il y en a une qui est levée
{
  echo 'Une exception a été lancée. Message d\'erreur : ', $e->getMessage();
}

Ce qui affichera :

Résultat affiché par le script
Résultat affiché par le script

Comme vous pouvez le constater, la troisième instruction du bloctryn'a pas été exécutée. C'est normal puisque la deuxième instruction a interrompu la lecture du bloc. Si vous interceptez les exceptions comme nous l'avons fait, alors le script n'est pas interrompu. En voici la preuve :

<?php
function additionner($a, $b)
{
  if (!is_numeric($a) || !is_numeric($b))
  {
    throw new Exception('Les deux paramètres doivent être des nombres');
  }
  
  return $a + $b;
}

try // Nous allons essayer d'effectuer les instructions situées dans ce bloc.
{
  echo additionner(12, 3), '<br />';
  echo additionner('azerty', 54), '<br />';
  echo additionner(4, 8);
}

catch (Exception $e) // Nous allons attraper les exceptions "Exception" s'il y en a une qui est levée.
{
  echo 'Une exception a été lancée. Message d\'erreur : ', $e->getMessage();
}

echo 'Fin du script'; // Ce message s'affiche, ça prouve bien que le script est exécuté jusqu'au bout.

 

Vous vous demandez sans doute pourquoi on doit spécifier le nom de l'exception à intercepter puisque c'est toujours une instance de la classeException. En fait, la classeExceptionest la classe de base pour toute exception qui doit être lancée, ce qui signifie que l'on peut lancer n'importe quelle autre instance d'une classe, du moment qu'elle hérite de la classeException !

Des exceptions spécialisées

Hériter la classe Exception

PHP nous offre la possibilité d'hériter la classeExceptionafin de personnaliser nos exceptions. Par exemple, nous pouvons créer une classeMonExceptionqui réécrira des méthodes de la classeExceptionou en créera de nouvelles qui lui seront propres.

Avant de foncer tête baissée dans l'écriture de notre classe personnalisée, encore faut-il savoir quelles méthodes et attributs seront disponibles. Voici la liste des attributs et méthodes de la classeExceptiontirée de la documentation :

<?php
class Exception
{
  protected $message = 'exception inconnu'; // Message de l'exception.
  protected $code = 0; // Code de l'exception défini par l'utilisateur.
  protected $file; // Nom du fichier source de l'exception.
  protected $line; // Ligne de la source de l'exception.
  
  final function getMessage(); // Message de l'exception.
  final function getCode(); // Code de l'exception.
  final function getFile(); // Nom du fichier source.
  final function getLine(); // Ligne du fichier source.
  final function getTrace(); // Un tableau de backtrace().
  final function getTraceAsString(); // Chaîne formattée de trace.
  
  /* Remplacable */
  function __construct ($message = NULL, $code = 0);
  function __toString(); // Chaîne formatée pour l'affichage.
}

Ainsi, nous voyons que l'on a accès aux attributs protégés de la classe et qu'on peut réécrire les méthodes__constructet__toString. Toutes les autres méthodes sont finales, nous n'avons donc pas le droit de les réécrire.

Nous allons donc créer notre classeMonExceptionqui, par exemple, réécrira le constructeur en rendant obligatoire le premier argument ainsi que la méthode__toStringpour n'afficher que le message d'erreur (c'est uniquement ça qui nous intéresse).

<?php
class MonException extends Exception
{
  public function __construct($message, $code = 0)
  {
    parent::__construct($message, $code);
  }
  
  public function __toString()
  {
    return $this->message;
  }
}

Maintenant, comme vous l'aurez peut-être deviné, nous n'allons pas lancer d'exceptionExceptionmais une exception de typeMonException.

Dans notre script, nous allons désormais attraper uniquement les exceptionsMonException, ce qui éclaircira le code car c'est une manière de se dire que l'on ne travaille, dans le bloctry, qu'avec des instructions susceptibles de lancer des exceptions de typeMonException. Exemple :

<?php
class MonException extends Exception
{
  public function __construct($message, $code = 0)
  {
    parent::__construct($message, $code);
  }
  
  public function __toString()
  {
    return $this->message;
  }
}

function additionner($a, $b)
{
  if (!is_numeric($a) || !is_numeric($b))
  {
    throw new MonException('Les deux paramètres doivent être des nombres'); // On lance une exception "MonException".
  }
  
  return $a + $b;
}

try // Nous allons essayer d'effectuer les instructions situées dans ce bloc.
{
  echo additionner(12, 3), '<br />';
  echo additionner('azerty', 54), '<br />';
  echo additionner(4, 8);
}

catch (MonException $e) // Nous allons attraper les exceptions "MonException" s'il y en a une qui est levée.
{
  echo $e; // On affiche le message d'erreur grâce à la méthode __toString que l'on a écrite.
}

echo '<br />Fin du script'; // Ce message s'affiche, ça prouve bien que le script est exécuté jusqu'au bout.

Ainsi, nous avons attrapé uniquement les exceptions de typeMonException. Essayez de lancer une exceptionExceptionà la place et vous verrez qu'elle ne sera pas attrapée. Si vous décidez d'attraper, dans le bloccatch, les exceptionsException, alors toutes les exceptions seront attrapées car elles héritent toutes de cette classe. En fait, quand vous héritez une classe d'une autre et que vous décidez d'attraper les exceptions de la classe parente, alors celles de la classe enfant le seront aussi.

Emboîter plusieurs blocscatch

Il est possible d'emboîter plusieurs blocscatch. En effet, vous pouvez mettre un premier bloc attrapant les exceptionsMonExceptionsuivi d'un deuxième attrapant les exceptionsException. Si vous effectuez une telle opération et qu'une exception est lancée, alors PHP ira dans le premier bloc pour voir si ce type d'exception doit être attrapé, si tel n'est pas le cas il va dans le deuxième, etc., jusqu'à ce qu'il tombe sur un bloc qui l'attrape. Si aucun ne l'attrape, alors une erreur fatale est levée. Exemple :

<?php
class MonException extends Exception
{
  public function __construct($message, $code = 0)
  {
    parent::__construct($message, $code);
  }
  
  public function __toString()
  {
    return $this->message;
  }
}

function additionner($a, $b)
{
  if (!is_numeric($a) || !is_numeric($b))
  {
    throw new MonException('Les deux paramètres doivent être des nombres'); // On lance une exception "MonException".
  }
  
  if (func_num_args() > 2)
  {
    throw new Exception('Pas plus de deux arguments ne doivent être passés à la fonction'); // Cette fois-ci, on lance une exception "Exception".
  }
  
  return $a + $b;
}

try // Nous allons essayer d'effectuer les instructions situées dans ce bloc.
{
  echo additionner(12, 3), '<br />';
  echo additionner(15, 54, 45), '<br />';
}

catch (MonException $e) // Nous allons attraper les exceptions "MonException" s'il y en a une qui est levée.
{
  echo '[MonException] : ', $e; // On affiche le message d'erreur grâce à la méthode __toString que l'on a écrite.
}

catch (Exception $e) // Si l'exception n'est toujours pas attrapée, alors nous allons essayer d'attraper l'exception "Exception".
{
  echo '[Exception] : ', $e->getMessage(); // La méthode __toString() nous affiche trop d'informations, nous voulons juste le message d'erreur.
}

echo '<br />Fin du script'; // Ce message s'affiche, cela prouve bien que le script est exécuté jusqu'au bout.

Cette fois-ci, aucune exceptionMonExceptionn'est lancée, mais une exceptionExceptionl'a été. PHP va donc effectuer les opérations demandées dans le deuxième bloccatch.

Exemple concret : la classe PDOException

Vous connaissez sans doute la bibliothèque PDO. Dans ce tutoriel, il vous est donné une technique pour capturer les erreurs générées par PDO : comme vous le savez maintenant, ce sont des exceptions ! Et PDO a sa classe d'exception :PDOException. Celle-ci n'hérite pas directement de la classeExceptionmais deRuntimeException. Cette classe n'a rien de plus que sa classe mère, il s'agit juste d'une classe qui est instanciée pour émettre une exception lors de l'exécution du script. Il existe une classe pour chaque circonstance dans laquelle l'exception est lancée : vous trouverez la liste des exceptions ici.

Cette classePDOExceptionest donc la classe personnalisée pour émettre une exception par la classe PDO ou PDOStatement. Sachez d'ailleurs que si une extension orientée objet doit émettre une erreur, elle émettra une exception.

Bref, voici un exemple d'utilisation dePDOException:

<?php
try
{
  $db = new PDO('mysql:host=localhost;dbname=tests', 'root', ''); // Tentative de connexion.
  echo 'Connexion réussie !'; // Si la connexion a réussi, alors cette instruction sera exécutée.
}

catch (PDOException $e) // On attrape les exceptions PDOException.
{
  echo 'La connexion a échoué.<br />';
  echo 'Informations : [', $e->getCode(), '] ', $e->getMessage(); // On affiche le n° de l'erreur ainsi que le message.
}

Exceptions pré-définies

Il existe toute une quantité d'exceptions pré-définies. Vous pouvez obtenir cette liste sur la documentation. Au lieu de lancer tout le temps une exception en instanciantException, il est préférable d'instancier la classe adaptée à la situation. Par exemple, reprenons le code proposé en début de chapitre :

<?php
function additionner($a, $b)
{
  if (!is_numeric($a) || !is_numeric($b))
  {
    // On lance une nouvelle exception grâce à throw et on instancie directement un objet de la classe Exception.
    throw new Exception('Les deux paramètres doivent être des nombres');
  }
  
  return $a + $b;
}

echo additionner(12, 3), '<br />';
echo additionner('azerty', 54), '<br />';
echo additionner(4, 8);

La classe à instancier ici est celle qui doit l'être lorsqu'un paramètre est invalide. On regarde la documentation, et on tombe surInvalidArgumentException. Le code donnerait donc :

<?php
function additionner($a, $b)
{
  if (!is_numeric($a) || !is_numeric($b))
  {
    throw new InvalidArgumentException('Les deux paramètres doivent être des nombres');
  }
  
  return $a + $b;
}

echo additionner(12, 3), '<br />';
echo additionner('azerty', 54), '<br />';
echo additionner(4, 8);

Cela permet de mieux se repérer dans le code et surtout de mieux cibler les erreurs grâce aux multiples blocscatch.

Exécuter un code même si l'exception n'est pas attrapée

Si jamais une exception est lancée dans un bloctry mais non attrapée dans un bloc catch , alors une erreur fatale sera levée car l'exception ne sera pas attrapée. Il existe une façon d'exécuter du code avant de lever l'erreur fatale grâce au bloc finally . Par exemple :

<?php
$db = new PDO('mysql:host=localhost;dbname=tests', 'root', '');

try
{
  // Quelques opérations sur la base de données
}
finally
{
  echo 'Action effectuée quoi qu\'il arrive';
}

Dans le bloc finally , on trouvera généralement des opérations de nettoyage, comme la fermeture d'un fichier ou d'une connexion par exemple. Cela permet de s'assurer que le script se terminera plus ou moins comme il faut.

Gérer les erreurs facilement

Convertir les erreurs en exceptions

Nous allons voir une dernière chose avant de passer au prochain chapitre : la manière de convertir les erreurs fatales, alertes et notices en exceptions. Pour cela, nous allons avoir besoin de la fonction set_error_handler. Celle-ci permet d'enregistrer une fonction en callback qui sera appelée à chaque fois que l'une de ces trois erreurs sera lancée. Il n'y a pas de rapport direct avec les exceptions : c'est à nous de l'établir.

Notre fonction, que l'on nommeraerror2exceptionpar exemple, doit demander entre deux et cinq paramètres :

  • Le numéro de l'erreur (obligatoire).

  • Le message d'erreur (obligatoire).

  • Le nom du fichier dans lequel l'erreur a été lancée.

  • Le numéro de la ligne à laquelle l'erreur a été identifiée.

  • Un tableau avec toutes les variables qui existaient jusqu'à ce que l'erreur soit rencontrée.

Nous n'allons pas prêter attention au dernier paramètre, juste aux quatre premiers. Nous allons créer notre propre classeMonExceptionqui hérite non pas deExceptionmais deErrorException. Bien sûr, comme je l'ai déjà dit plus haut, toutes les exceptions héritent de la classeException:ErrorExceptionn'échappe pas à la règle et hérite de celle-ci.

La fonctionset_error_handlerdemande deux paramètres. Le premier est la fonction à appeler, et le deuxième, les erreurs à intercepter. Par défaut, ce paramètre intercepte toutes les erreurs, y compris les erreurs strictes.

Le constructeur de la classeErrorExceptiondemande cinq paramètres, tous facultatifs :

  • Le message d'erreur.

  • Le code de l'erreur.

  • La sévérité de l'erreur (erreur fatale, alerte, notice, etc.) représentées par des constantes pré-définies.

  • Le fichier où l'erreur a été rencontrée.

  • La ligne à laquelle l'erreur a été rencontrée.

Voici à quoi pourrait ressembler le code de base :

<?php
class MonException extends ErrorException
{
  public function __toString()
  {
    switch ($this->severity)
    {
      case E_USER_ERROR : // Si l'utilisateur émet une erreur fatale;
        $type = 'Erreur fatale';
        break;
      
      case E_WARNING : // Si PHP émet une alerte.
      case E_USER_WARNING : // Si l'utilisateur émet une alerte.
        $type = 'Attention';
        break;
      
      case E_NOTICE : // Si PHP émet une notice.
      case E_USER_NOTICE : // Si l'utilisateur émet une notice.
        $type = 'Note';
        break;
      
      default : // Erreur inconnue.
        $type = 'Erreur inconnue';
        break;
    }
    
    return '<strong>' . $type . '</strong> : [' . $this->code . '] ' . $this->message . '<br /><strong>' . $this->file . '</strong> à la ligne <strong>' . $this->line . '</strong>';
  }
}

function error2exception($code, $message, $fichier, $ligne)
{
  // Le code fait office de sévérité.
  // Reportez-vous aux constantes prédéfinies pour en savoir plus.
  // http://fr2.php.net/manual/fr/errorfunc.constants.php
  throw new MonException($message, 0, $code, $fichier, $ligne);
}

set_error_handler('error2exception');

Vous voyez que dans la méthode__toStringje mettais à chaque fois E_X et E_USER_X. Les erreurs du type E_X sont générées par PHP et les erreurs E_USER_X sont générées par l'utilisateur grâce à trigger_error. Les erreurs E_ERROR (donc les erreurs fatales générées par PHP) ne peuvent être interceptées, c'est la raison pour laquelle je n'ai pas placé ce type dans le switch.

Ensuite, à vous de faire des tests, vous verrez bien que ça fonctionne à merveille. Mais gardez bien ça en tête : avec ce code, toutes les erreurs (même les notices) qui ne sont pas dans un bloctry interrompront le script car elles émettront une exception !

On aurait très bien pu utiliser la classeExceptionmaisErrorExceptiona été conçu exactement pour ce genre de chose. Nous n'avons pas besoin de créer d'attribut stockant la sévérité de l'erreur ou de réécrire le constructeur pour y stocker le nom du fichier et la ligne à laquelle s'est produite l'erreur.

Personnaliser les exceptions non attrapées

Nous avons réussi à transformer toutes nos erreurs en exceptions en les interceptant grâce àset_error_handler. Étant donné que la moindre erreur lèvera une exception, il serait intéressant de personnaliser l'erreur générée par PHP. Ce que je veux dire par là, c'est qu'une exception non attrapée génère une longue et laide erreur fatale. Nous allons donc, comme pour les erreurs, intercepter les exceptions grâce àset_exception_handler. Cette fonction demande un seul argument : le nom de la fonction à appeler lorsqu'une exception est lancée. La fonction de callback doit accepter un argument : c'est un objet représentant l'exception.

Voici un exemple d'utilisation en reprenant le précédent code :

<?php
class MonException extends ErrorException
{
  public function __toString()
  {
    switch ($this->severity)
    {
      case E_USER_ERROR : // Si l'utilisateur émet une erreur fatale.
        $type = 'Erreur fatale';
        break;
      
      case E_WARNING : // Si PHP émet une alerte.
      case E_USER_WARNING : // Si l'utilisateur émet une alerte.
        $type = 'Attention';
        break;
      
      case E_NOTICE : // Si PHP émet une notice.
      case E_USER_NOTICE : // Si l'utilisateur émet une notice.
        $type = 'Note';
        break;
      
      default : // Erreur inconnue.
        $type = 'Erreur inconnue';
        break;
    }
    
    return '<strong>' . $type . '</strong> : [' . $this->code . '] ' . $this->message . '<br /><strong>' . $this->file . '</strong> à la ligne <strong>' . $this->line . '</strong>';
  }
}

function error2exception($code, $message, $fichier, $ligne)
{
  // Le code fait office de sévérité.
  // Reportez-vous aux constantes prédéfinies pour en savoir plus.
  // http://fr2.php.net/manual/fr/errorfunc.constants.php
  throw new MonException($message, 0, $code, $fichier, $ligne);
}

function customException($e)
{
  echo 'Ligne ', $e->getLine(), ' dans ', $e->getFile(), '<br /><strong>Exception lancée</strong> : ', $e->getMessage();
}

set_error_handler('error2exception');
set_exception_handler('customException');

En résumé

  • Une exception est une erreur que l'on peut attraper grâce aux mots-clétryetcatch.

  • Une exception est une erreur que l'on peut personnaliser, que ce soit au niveau de son affichage ou au niveau de ses renseignements (fichier concerné par l'erreur, le numéro de la ligne, etc.).

  • Une exception se lance grâce au mot-cléthrow.

  • Il est possible d'hériter des exceptions entre elles.

  • Il existe un certain nombre d'exceptions déjà disponibles dont il ne faut pas hésiter à se servir pour respecter la logique du code.

Exemple de certificat de réussite
Exemple de certificat de réussite