Les exceptions - 2ème partie

Rechercher

Les exceptions - 2ème partie

  • Par Emacs
  • 8 commentaires
  • 15544 lectures
  • RSS -  Atom

La première partie de ce tutoriel a été l'occasion de présenter le mécanisme des exceptions de manière très théorique. Au travers d'exemples simples et concrets, nous avons découvert comment générer, lancer et attraper des exceptions en plein vol. A ce stade, nous sommes encore loin de pouvoir profiter pleinement des exceptions dans des applications plus conséquente. C'est pourquoi cette seconde et dernière partie s'intéressera à la manière de dériver la classe Exception pour créer des exceptions personnalisées. Enfin, nous étudierons un mécanisme natif de PHP qui permet de centraliser et d'unifier le traitement des exceptions non capturées dans une fonction de callback appelée automatiquement par l'exception handler.

Dériver la classe Exception et créer des exceptions personnalisées

Principe de création de classes personnalisées

Cette partie du tutoriel est sans aucun doute la plus triviale. Elleexplique comment dériver la classe Exception (via le concept del'héritage de programmation orientée objet) afin de générer ensuite desexceptions personnalisées. En effet, comme une classe = un type,il est alors possible de créer de nouvelles classes (donc denouveaux types) qui héritent des propriétés et méthodes de la classeException.

Quel sont les avantages à créer des classes dérivées ? La raison estsimple. Avoir plusieurs types d'exceptions permet tout d'abord de lesdistinguer plus facilement. En distinguant chaque type d'erreur, on peut appliquer des traitements appropriés différents.Enfin, la dérivation de la classe Exception permet au développeur desurchager l'objet en lui ajoutant des propriétés et méthodessupplémentaires. Le code ci-dessous présente la manière la plus simplede développer une nouvelle classe d'exception personnalisée.

Principe de création d'une classe dérivée de Exception
<?php
/**
* Fichier MyChildException.class.php
*/
class MyChildException extends Exception
{
public function __construct($message=NULL, $code=0)
{
parent::__construct($message, $code);
}
}
?>

Tout d'abord le nom de la nouvelle classe est déclarée (MyChildException) ainsi que la classe mère (Exception) qu'elle dérive et dont elle hérite les propriétés et méthodes. C'est grâce au mot-clé extends que l'héritage a lieu.

Puis nous redéfinissons la méthode constructeur de la classe parente Exception. Lorsque le constructeur de la classe MySuperException sera appelé, il appellera automatiquement le constructeur de la classe parente. Cela aura pour effet de créer un objet de type MySuperException mais aussi de type Exception par héritage.

Lorsque l'on redéfinit le constructeur parent, il faut donc bienévidemment penser à récupérer au moins ses paramètres pour lesréinjecter dans le constructeur fils.

Exemples de classes d'exceptions personnalisées

Développons maintenant à titre d'exemple deux classes personnaliséespour gérer les exceptions liées aux fichiers. La première classe permetde contrôler l'inexistence d'un fichier sur un serveur tandis que laseconde permettra de générer des erreurs indiquant que le fichier n'estpas accessible en écriture. Ajoutons en plus à chacune de ces deuxclasses, un nouvel attribut stockant la date à laquelle s'est produitel'erreur et un accesseur pour récupérer cette valeur. Nous obtenonsdonc :

Classes FileNotFoundException et FileNotWriteableException
<?php
/**
* Fichier FileNotFoundException.class.php
*/
class FileNotFoundException extends Exception
{
protected $timestamp;
public function __construct($message=NULL, $code=0)
{
parent::__construct($message, $code);
$this->timestamp = time();
}
public function getTimestamp() {
return $this->timestamp;
}
}
/**
* Fichier FileNotWriteableException.class.php
*/
class FileNotWriteableException extends Exception
{
protected $timestamp;
public function __construct($message=NULL, $code=0)
{
parent::__construct($message, $code);
$this->timestamp = time();
}
public function getTimestamp() {
return $this->timestamp;
}
}
?>

Profitons tout de suite de l'héritage pour faire dériver ces deux classes de la même classe FileExceptionet qui contiendra l'attribut et la méthode commune. Cette nouvelleclasse, quant à elle, dérivera bien entendu la classe native Exception.

Classes de gestion des erreurs liées au fichiers
<?php
/**
* Fichier FileException.class.php
*/
class FileException extends Exception
{
protected $timestamp;
public function __construct($message=NULL, $code=0)
{
parent::__construct($message, $code);
$this->timestamp = time();
}
public function getTimestamp() {
return $this->timestamp;
}
}
/**
* Fichier FileNotFoundException.class.php
*/
class FileNotFoundException extends FileException
{
public function __construct($message=NULL, $code=0)
{
parent::__construct($message, $code);
$this->timestamp = time();
}
}
/**
* Fichier FileNotWriteableException.class.php
*/
class FileNotWriteableException extends FileException
{
public function __construct($message=NULL, $code=0)
{
parent::__construct($message, $code);
$this->timestamp = time();
}
}
?>

L'exemple qui suit montre comment récupérer chaque type d'exceptionavec plusieurs blocs catch() associés au même bloc try {}. Grâce à cesblocs catch() en ligne et les différents types d'exceptions, il estalors très simple de reconnaître les erreurs levées et donc exécuterles traitements les plus adaptés.

Exemple d'utilisation des 3 classes de gestion des erreurs de fichier
<?php
// Import des 3 classes précédentes
require_once(dirname(__FILE__).'/FileException.class.php');
require_once(dirname(__FILE__).'/FileNotFoundException.class.php');
require_once(dirname(__FILE__).'/FileNotWriteableException.class.php');
// Variables
$fichier = '/var/www/projet/toto.txt';
try
{
// Le fichier existe-t-il ?
if(!file_exists($fichier)) {
throw new FileNotFoundException('Le fichier '. $fichier .' est inexistant');
}
// Le fichier est-il inscriptible ?
if(!is_writeable($fichier)) {
throw new FileNotWriteableException('Le fichier '. $fichier .' n\'a pas les droits d\'écriture');
}
// A-t-on ouvert le fichier en mode écriture ?
if(!($fp = @fopen($fichier,'w'))) {
throw new FileException('L\'ouverture du fichier '. $fichier .' a échoué');
}
// J'écris dans mon fichier
fwrite($fp, "Coucou Emacs\n");
// Puis je ferme mon fichier
fclose($fp);
}
catch(FileNotFoundException $e)
{
// Je crée le fichier
}
catch(FileNotWriteableException $e)
{
// Je change les droits du fichier
}
catch(FileException $e)
{
// Je stoppe tout
exit($e->getMessage());
}
catch(Exception $e)
{
// Je stoppe tout
exit($e->getMessage());
}
?>

Vous remarquez ici les quatre blocs catch() en ligne permettantd'intercepter les trois types d'exceptions potentiellement jetablesdepuis le bloc try(). En précisant le type de l'exception interceptéedans le bloc catch(), nous savons à quel genre d'erreur nous avonsaffaire. N'est-ce pas plus clair et maintenable à présent ?

Note : il est important de conserverl'ordre des blocs catch(), c'est-à-dire de mettre en premier le bloccatch() indiquant l'erreur la plus précise (donc la fille la plus bassepar héritage). De ce fait, la classe Exception, la plus générale, doitarriver dans le dernier bloc catch(). Si le bloc catch() du typeException avait été placé avant les autres, alors toutes les exceptions(tout type confondu) auraient toujours été levées dans celui-ci. Eneffet, une exception de type FileNotFoundException est aussi parhéritage une exception de type FileException et donc aussi uneexception de type Exception.

Centraliser le traitement des erreurs non capturées

Présentation du mécanisme d'interception automatique des exceptions

Jusqu'à maintenant nous savons que le moyen le plus simple de capturer une exception lancée est d'avoir recours à des blocs try { } catch() { }. Rappelez-vous le tout premier exemple de la 1ère partie du tutoriel, l'exception générée n'était pas capturable donc elle était forcément perdue à jamais.

Exemple de lancement d'une Exception à travers le programme
<?php
$password = 'Toto';
if('Emacs' !== $password) {
throw new Exception('Votre password est incorrect !');
}
// Cette ligne ne sera jamais exécutée
// car une exception est lancée pour interrompre
// l'exécution normale du programme
echo 'Bonjour Emacs';
?>

Heureusement PHP dispose d'un mécanisme qui permet de capturer automatiquement toutes les exceptions qui sont lancées mais qui ne sont pas entourées de blocs try {} catch() {} comme c'est le cas dans le listing ci-dessus. Il s'agit de l'exception handler.

L'exception handler, lorsqu'il intercepte une exception, interrompt complètement l'exécution du programme et appelle une fonction personnalisée de callback qui se chargera du traitement adéquat de ces exceptions perdues. Le code ci-après illustre sa mise en place.

Mise en place du mécanisme d'interception automatique des exceptions
<?php
/**
* Fonction de rappel appellée automatiquement par l'exception handler
*
* @param Exception $e une exception lancée et perdue dans le programme
* @return void
*/
function traitementExceptionPerdue(Exception $e) {
echo 'Une exception orpheline a été attrapée : ';
echo $e->getMessage(), "\n";
exit;
}
/**
* Enregistrement de la fonction de rappel dans l'exception handler de PHP
*/
set_exception_handler('traitementExceptionPerdue');
// Exemple de génération d'exception perdu
$password = 'Toto';
if('Emacs' !== $password) {
throw new Exception('Votre password est incorrect !');
}
// Cette ligne ne sera jamais exécutée
// car une exception est lancée pour interrompre
// l'exécution normale du programme
echo 'Bonjour Emacs';
?>

Quelques implications s'imposent car le principe n'est pas si simple à assimiler. Dans un premier temps, nous déclarons la fonction de rappel qui personnalisera le traitement des exceptions orphelines interceptées. C'est une fonction utilisateur qui prend un seul et unique paramètre. Ce paramètre n'est autre qu'un objet de type Exception (ou type dérivé d'Exception). Le corps de la fonction effectue les traitement particuliers pour les exceptions orphelines. Ici nous affichons simplement un message d'erreur, suivi du message de l'exception. Puis nous stoppons strictement toute la suite du programme PHP. Nous aurions pu par exemple redirigé automatiquement l'utilisateur vers une page d'erreur de type 500 en utilisant la fonction header().

Note : en PHP 5, tous les objets sont automatiquement passés par référence (pointeur) en paramètre de fonction. Il n'est donc pas nécessaire de les préfixer du symbole & dans la liste des paramètres de la signature.

La seconde partie de ce listing concerne l'enregistrement du nom de cette fonction comme gestionnaire d'exception par défaut à appeller automatiquement à la capture d'une exception orpheline. Il suffit simplement d'appeller la fonction set_exception_handler() de PHP, et de lui indiquer en paramètre le nom de la fonction traitementExceptionPerdue(). PHP prend ensuite le relais à la place du développeur.

Note : PHP interprête le code PHP en le lisant de haut en bas, c'est pourquoi il faut toujours déclarer la fonction de rappel avant d'appeller la fonction set_exception_handler().

Enfin la dernière partie reprend l'exemple de code qui génère une exception non capturable. Au moment où l'exception est levée, PHP appelle automatiquement la fonction de rappel en lui passant en paramètre l'exception orpheline qui vient d'être générée. Le résultat ci-après est obtenu sur la sortie standard :

Une exception orpheline a été attrapée : Votre password est incorrect !

Le mécanisme, bien que peu facile à assimiler de prime abord, se révèle très simple à mettre en place et efficace pour gérer les cas exceptionnels inattendus. Grâce à cet outil, les développeurs peuvent écrire une fonction complète pour faciliter le debugging en affichant par exemple tout le contexte d'éxécution du programme : variables globales, fichiers concernés, pile des appels de fonctions...

Effets de bord néfastes avec set_exception_handler

Attention, l'utilisation de set_exception_handler() doit être utilisé avec prudence car elle peut entraîner des effets de bords particulièrement gênants si elle est mal maîtrisée. En effet, si le corps de la fonction de rappel fait appel à d'autres fonctions ou méthodes susceptibles de lancer des exceptions, le gestionnaire d'exception sera solllicité en boucle. C'est la boucle infinie !!! Il faut donc s'assurer que le corps de la fonction de callback n'exécute pas de traitements susceptibles de générer des exceptions non maîtrisées.

Inconvénients et limitation des exceptions en PHP

L'utilisation des exceptions apporte un intérêt non négligeable au développeur lorsqu'il développe son application. Nous avons compris tout au long de ce tutoriel que ce mécanisme permettait d'identifier clairement les types d'erreurs générées dans le but de les traiter spécifiquement et efficacement. Néanmoins, nous pouvons relever quelques limitations au sujet des exceptions :

  • PHP 5 n'exploite pas en natif les exceptions. C'est-à-dire que les fonctions PHP continuent de générer des erreurs et non des exceptions. De ce fait, le développeur est obligé de gérer à la fois les exceptions et les erreurs PHP dans ses procédures de debugging.
  • Lorsqu'une exception est soulevée, le contexte local contenu dans un bloc try est sauvegardé, ceci peut être amplifié par l'imbrication de bloc try. L'abus de l'utilisation des exceptions peut donc diminuer considérablement les performances. (extrait du tutoriel des exceptions sur le site Developpez.com)
  • Par choix. La plupart des langages choisissent de ne pas supprimer la gestion des erreurs au profit des exceptions pour des raisons de performances et de style de programmation (les exceptions sont équivalentes pour certains programmateurs à l'affreux goto, de plus les exceptions augmentent considérablement le nombre de lignes de code). D'autres langages, comme l'ADA, n'utilisent que les exceptions. (extrait du tutoriel des exceptions sur le site Developpez.com)

Conclusion

Nous arrivons à l'issue de ce tutoriel concernant la manipulation des exceptions. Depuis le début, nous avons appris à générer une exception, la lancer à travers le programme puis l'intercepter dans un bloc try {} catch() {} en vue d'un traitement spécifique. Puis nous nous sommes intéressés à la création d'exceptions personnalisées dans le but de faciliter la compréhension du code et l'identification des types d'erreurs générées dans le programme. Enfin nous nous sommes arrêtés sur le mécanisme d'interception automatique d'exception orpheline intégré nativement dans le langage PHP. Nous sommes donc parés pour développer des applications orientées objets capables de générer et de traiter avec adéquation les cas exceptionnels d'erreur.



Les commentaires

1. Par Michael le 31/03/2008 17:57

Article presque parfait, à l'exception (c'est le cas de le dire) du fait que cela ne fonctionne pas

parent::__construct($message=NULL,$code=0);
// va litérallement "ecraser" le message à null et le code à 0.

Bonne syntaxe : parent::__construct($message,$code);

Cela vous arrive de tester vos codes ?

2. Par Emacs le 01/04/2008 00:17

Oups c'est exact... Au temps pour moi ! Merci Michaël pour la correction. Il s'agissait en fait de vilains copiés / collés.

Je n'ai pas testé ces codes car ce sont des codes que je maîtrise (d'habitude.... lol). J'aurais du me relire davantage

Encore merci pour ta remarque

Hugo.

3. Par Julien Pellegrain le 02/05/2008 08:05

J'ajouterais que le code n'est pas du tout optimisé.
Tu écris "Profitons de l'héritage" pour créer 2 classes filles à ta classe FileException mais tu réécris à l'identique les constructeurs ! Cela ne sert à rien...

Un simple
class MaClasse extends FileException {} suffit

4. Par Emacs le 02/05/2008 09:13

@Julien : Oui je sais que le constructeur parent d'une classe fille est appelé implicitement et qu'il n'est pas nécessaire de l'écrire explicitement. Il est nécessaire quand on veut surcharger le constructeur fils. Ici, c'est ce que j'ai fait si tu remarques bien. J'ai ajouté un attribut $timestamp dans lequel je stocke la date à laquelle a été levée l'exception. Je suis donc obligé à ce moment là d'appeler explicitement le constructeur parent.

Enfin, je dirai qu'appeler ou non explicitement le constructeur parent lorsque l'on dérive une classe est une question de goût et de bonnes pratiques. En Java, me semble-t-il, l'appelle au mot-clé super() est obligatoire pour dériver. Donc avec PHP je fais pareil. J'appelle le constructeur parent explicitement. Mais je suis d'accord avec toi que ce n'est pas forcément nécessaire

5. Par mRk le 31/07/2008 16:41

"Exxemple d'utilisation des 3 classes de gestion des erreurs de fichier"

Un petit 'x' de trop dans 'exemple'.

6. Par Emacs le 01/08/2008 13:01

Corrigé ! Merci mRk

7. Par Clebert le 05/09/2008 12:12

Très bon tutorial, j'ai bien compris le système. Par contre, il est dommage de ne pas pousser les choses plus loin, en illustrant avec une classe de Log par exemple. C'est ce que je suis en train de faire et j'avoue galérer légèrement ...

8. Par krifur le 22/01/2009 17:10

Super site, super infos, super tutos, super super.
Petite coquille je pense
"Quelques implications s'imposent (...)"
=>
"Quelques explications s'imposent (...)"