Bonjour à tous,
Vu que je reçoit régulièrement des question de ce genre par MP (même si je m'étais juré de ne pas répondre aux questions technique, c'est plus fort que moi ~SEGAAA) je pense qu'il est utile que je fasse le point sur la gestion des erreurs et des exceptions en PHP.
Les remarques qui suivent sont à l'origine d'un MP adressé par BobbyMcGee, si vous avez vous-même des intérrogation sur le sujet, n'hésitez pas à poster en enfilade (plutôt que d'envoyer des MP !)
Théorie de la gestion d'erreur en PHP
Je suis habitué à des langages comme Java ou C# où toute erreur remonte sous la forme d'une exception. J'ai lu quelque part que seules les extensions récentes de PHP (type PDO) lèvent des exceptions en cas d'erreur. Est-ce exact ? Comment gérer les autres erreurs ?
En effet, les exceptions étant apparues avec la version 5 du langage, seules les extensions développées depuis lancent des exceptions. Pour ce qui est de PDO, le mécanisme par défaut est de lever des erreurs (E_WARNING), il faut lui spécifier explicitement l'utilisation des exceptions avec
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
Comme tu le sais, PHP n'est pas un langage compilé. Du coup, les erreurs de syntaxe (et certaines erreurs de runtime) ne peuvent pas être détectées par le compilateur. C'est pourquoi PHP lève des erreurs (E_NOTICE, E_WARNING et la plus grave E_FATAL_ERROR qui indique que la state machine est dans un état inconnu), il est d'ailleurs possible de lever manuellement ces erreurs avec trigger_error (en utilisant les codes E_USER_x). Ces erreurs peuvent être traitées lors du runtime à l'aide des fonctions error_get_last ou encore set_error_handler. Mais ce mécanisme présente le défaut de ne pas permettre la remontée de l'erreur (et donc l'arrêt de l'exécution du bloc en cours). C'est pourquoi la version 5 intègre les exceptions.
En PHP; il n'existe que 3 façons admises de gérer les cas d'erreurs:
- en renvoyant un statut d'erreur (par exemple false) mais cela à l'inconvénient de ne pas être très explicite sur l'erreur elle-même
- en émettant une erreur PHP mais cela à l'inconvénient de ne pas arrêter l'exécution en cours (sauf avec E_USER_ERROR qui est l'équivalent de E_ERROR et qui bloque le script), de plus les erreurs sont loggées dans le log d'erreur d'apache par défaut...
- en levant une exception
Ex:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| <?php
/**
* Dans ce cas, on renvoie false en cas d'erreur mais on ne signale pas ce qui ne va pas.
* @return int, false in case of error
*/
function square ($a) {
if (!$a)
return false;
if (!is_int($a))
return false;
return pow($a, 2);
}
/**
* Dans ce cas, on renvoie false et on signale ce qui s'est mal passé avec une erreur
* @return int, false in cas of error
*/
function square ($a) {
if (!$a) {
trigger_error("invalid value", E_USER_WARNING);
return false;
}
if (!is_int($a)) {
trigger_error("invalid type", E_USER_WARNING);
return false;
}
return pow($a, 2);
}
/**
* Dans ce cas, on ne renvoie rien et on lance une exception
* @throw UnexpectedValueException if $a is empty
* @throw InvalidArgumentExceptino if $a is not integer
* @return int
*/
function square ($a) {
if (!$a)
throw new UnexpectedValueException("invalid value for a");
if (!is_int($a))
throw new InvalidArgumentException("invalid type for a");
return pow($a, 2);
} |
La gestion par erreur + return ressemble de près à la gestion par exception (à ceci près que les erreurs ne remontent pas tant qu'elles ne sont pas catchées comme les exceptions) et dans beaucoup de cas, cette gestion peut suffire. Il existe en revanche des cas ou ce n'est pas possible, ce sont pour ces cas exceptionnels que tous les langages objet du monde implémentent les exceptions. C'est notamment le cas du constructeur, si une erreur était détectée pendant la construction d'un objet, que faudrait il faire vu que les constructeurs ne retournent rien (même pas void) ?
On peut effectivement toujours s'en sortir avec un paramètre out:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <?php
class Foo {
protected $_value;
public function __construct ($a, & $return = null) {
if (!$a) {
$return = false;
trigger_error("invalid value", E_USER_WARNING);
return;
}
if (!is_int($a)) {
$return = false;
trigger_error("invalid type", E_USER_WARNING);
return false;
}
$this->_value = pow($a, 2);
}
} |
Mais c'est outrageusement impropre, ça complexifie inutilement le code de notre constructeur et ça rajoute des paramètres pas bien clairs sur son prototype. Il vaut mieux lever une exception:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php
class Foo {
protected $_value;
public function __construct ($a) {
if (!$a)
throw new UnexpectedValueException("invalid value for a");
if (!is_int($a))
throw new InvalidArgumentException("invalid type for a");
$this->_value = pow($a, 2);
}
} |
Pour pouvoir attraper une exception, il faut impérativement qu'elle soit lancée à l'intérieur d'un bloc try / catch. En PHP, il est possible de catcher plusieurs fois:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?php
function foo () {
bar();
}
function bar () {
throw new LogicException("aha");
}
try {
foo();
}
catch (RuntimException $e) {
// do something...
}
catch (InvalidArgumentException $e) {
// do something else...
}
catch (Exception $e) { // ~pokééémooooon (gotta catch em all)
// attrape tout (car toute classe d'exception dérive d'Exception)
} |
On notera également que les blocs try / catch peuvent s'imbriquer théoriquement à l'infini, ce qui fait qu'un catch peut tout à fait effectuer un rethrow, ce qui est utile car depuis PHP 5.3 on peut définir pour une exception, quelle à été l'exception d'origine ($previous).
Ex:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <?php
function foo () {
try {
bar();
}
catch (Exception $e) {
throw new Exception("foo exception", 0, $e);
}
}
function bar () {
throw new Exception("bar exception");
}
try {
foo();
}
catch (Exception $e) {
var_dump($e->getPrevious());
} |
Je ne le répéterai jamais assez mais les exceptions sont exceptionnelles ! Elles doivent êtres utilisées quand rien d'autre n'est possible (ou plutôt sémantiquement envisageable - car tout est toujours possible en informatique).
Gestion des erreurs
Existe-t-il un moyen de basculer en mode "full exception" afin d'unifier la gestion des erreurs ?
Oui, c'est possible avec set_error_handler:
Ex:
1 2 3 4 5 6 7
| <?php
function handle_error ($errno, $errstr, $errfile, $errline) {
throw new ErrorException($errstr, 2048, $errno, $errfile, $errline);
}
set_error_handler('handle_error'); |
Mais ce n'est pas toujours souhaitable car comme on l'a vu, les exception on un mécanisme différent des erreurs. De plus, PHP à tendance à lever des erreurs pour presque n'importe quoi (E_DEPRECATED, E_NOTICE, E_STRICT sont des erreurs mineures). C'est donc un mécanisme à utiliser avec des script dits E_STRICT safe qu'on contrôle de bout en bout. Généralement, on se sert plus volontiers de set_error_handler pour du logging.
Ex:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
... ma classe de log
public static warning ($message) {
// écrit dans le fichier de log
}
...
public static function handleError ($errno, $errstr, $errfile, $errline) {
$error = "(PHP Error) $errstr in $errfile on line $errline";
switch ($errno) {
case E_WARNING:
case E_USER_WARNING:
self::warning($error);
break;
case E_NOTICE:
case E_USER_NOTICE:
self::notice($error);
break;
default:
case E_USER_ERROR:
self::error($error);
break;
case E_RECOVERABLE_ERROR:
throw new ErrorException($errstr, 2048, $errno, $errfile, $errline);
break;
}
} |
On notera dans mon dernier exemple le cas du E_RECOVERABLE_ERROR, il s'agit d'un cas particulier d'erreur qu'on est capable de "catcher". On rencontre cette erreur par exemple lorsqu'une méthode __toString n'a pas renvoyé une chaîne de caractères comme elle devrait. Convertir cette erreur en exception est plutôt une bonne idée.
Conclusion
En aucun cas les exceptions ne se substituent au mécanisme traditionnel de gestion des erreurs de PHP. Aucun développeur PHP ne saurait ignorer l'un des deux mécanismes tant ils sont complémentaires. Les exception, de par leur puissance, sont un moyen fiable et précis de gêrer les erreurs mais il faut savoir ne pas en abuser, et c'est malheureusement quelque chose que je ne peux pas vous apprendre, vous devez le découvrir par la pratique.
Faites vous même quelques essais pour vous familiariser avec tout ça, n'oubliez pas de rajouter
1 2 3
| <?php
ini_set('error_reporting', -1);
ini_set('display_errors', 1); |
au début de votre script afin que toutes les erreurs soient affichées, vous devriez avoir quelques surprises
Dans tout les cas, n'oubliez jamais qu'a chaque fois que vous écrivez or die(...) dans un script, je tue un chaton (et je ne veux pas de débat sur l'utilisabilité du or die dans ce thread, vous êtes prévenus ) !
Partager