Les exceptions - 2ème partie - Apprendre-PHP.com

Rechercher
Boutique en ligne, solution e-commerce, script PHP et PERL : RAYNETTE

Les exceptions - 2ème partie

Les exceptions - 2ème partie

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. Elle explique comment dériver la classe Exception (via le concept de l'héritage de programmation orientée objet) afin de générer ensuite des exceptions personnalisées. En effet, comme une classe = un type, il est alors possible de créer de nouvelles classes (donc de nouveaux types) qui héritent des propriétés et méthodes de la classe Exception.

Quel sont les avantages à créer des classes dérivées ? La raison est simple. Avoir plusieurs types d'exceptions permet tout d'abord de les distinguer 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 de surchager l'objet en lui ajoutant des propriétés et méthodes supplémentaires. Le code ci-dessous présente la manière la plus simple de 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 les réinjecter dans le constructeur fils.

Exemples de classes d'exceptions personnalisées

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

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 FileException et qui contiendra l'attribut et la méthode commune. Cette nouvelle classe, 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'exception avec plusieurs blocs catch() associés au même bloc try {}. Grâce à ces blocs catch() en ligne et les différents types d'exceptions, il est alors très simple de reconnaître les erreurs levées et donc exécuter les traitements les plus adaptés.

Exxemple 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 permettant d'intercepter les trois types d'exceptions potentiellement jetables depuis le bloc try(). En précisant le type de l'exception interceptée dans le bloc catch(), nous savons à quel genre d'erreur nous avons affaire. N'est-ce pas plus clair et maintenable à présent ?

Note : il est important de conserver l'ordre des blocs catch(), c'est-à-dire de mettre en premier le bloc catch() indiquant l'erreur la plus précise (donc la fille la plus basse par héritage). De ce fait, la classe Exception, la plus générale, doit arriver dans le dernier bloc catch(). Si le bloc catch() du type Exception avait été placé avant les autres, alors toutes les exceptions (tout type confondu) auraient toujours été levées dans celui-ci. En effet, une exception de type FileNotFoundException est aussi par héritage une exception de type FileException et donc aussi une exception 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 lundi 31 mars 2008 à 17:05

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

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 ? Emoticone content

2. Par Emacs le mardi 01 avril 2008 à 00:12

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 Emoticone déçu

Encore merci pour ta remarque Emoticone clin d'oeil

Hugo.

3. Par Julien Pellegrain le vendredi 02 mai 2008 à 08:08

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 vendredi 02 mai 2008 à 09:09

@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 Emoticone content

Ajouter un commentaire

Votre avis
Se souvenir de mes informations