En route vers PHP 5.5 : Bloc finally pour les exceptions
23 octobre 2012 —C’est sans aucun doute une des fonctionnalités qui a le plus fait parler d’elle1 depuis qu’elle a été commitée sur la branche master
de PHP (ce qui signifie qu’il est probable qu’elle soit présente dans PHP 5.5) courant août : après des années d’attente, PHP supporte, finalement2, un bloc finally
pour les exceptions.
Ajout d’une construction finally
Là où PHP supporte, depuis sa version 5, une gestion d’exceptions basées sur les mots-clefs try
et catch
(le second correspondant au bloc de code exécuté en cas d’exception), la version actuelle de la branche master
rajoute un troisième mot-clef : finally
.
Le code positionné au sein du bloc finally
sera exécuté à la fin du code se trouvant dans le bloc try
, ou à la fin du code se trouvant dans le bloc catch
si une exception a été levée puis interceptée.
L’objectif principal est de permettre de garantir qu’une portion de code, typiquement de nettoyage (fermeture de connexion réseau, de fichier, libération de ressources, …), soit toujours exécutée, qu’une exception ait été ou non levée et/ou gérée.
Par exemple, si on considère le code suivant, où aucune exception n'est levée depuis le bloc `try` :
echo "avant\n";
try {
echo "try\n";
}
catch (Exception $e) {
// Pas d'exception lancée dans le bloc try
// => On ne passe pas ici
echo "catch\n";
}
finally {
echo "finally\n";
}
echo "apres\n";
Le code correspondant au bloc try
sera exécuté ; celui du bloc catch
ne le sera pas (puisqu’aucune exception n’aura été interceptée) ; et viendra ensuite le tour du code correspondant au bloc finally
, avant que l’exécution du script ne reprenne de façon normale.
Nous obtiendrons donc la sortie suivante :
avant
try
finally
apres
Dans l’autre sens maintenant, si une exception est levée depuis le bloc try
:
echo "avant\n";
try {
echo "try : avant throw\n";
throw new Exception("Boum !");
// Une exception a été levée => on ne passe pas ici
echo "try : apres throw";
}
catch (Exception $e) {
// Exception lancée dans le bloc try
// => On passe ici
echo "catch\n";
}
finally {
echo "finally\n";
}
echo "apres\n";
Comme nous pouvons nous y attendre, l’exécution du code se trouvant dans le bloc try
cesse dès la levée de l’exception, et l’exécution passe au bloc catch
– pas de nouveauté à ce niveau, c’est ainsi que fonctionne la gestion des exceptions depuis PHP 5.
Par contre, la nouveauté est que suite à l’exécution du code se trouvant dans le bloc catch
, celui se trouvant dans le bloc finally
est à son tour exécuté, avant que l’exécution du script ne suive son cours normal.
avant
try : avant throw
catch
finally
apres
Autrement dit, comme nous venons de le voir avec ces deux exemples, le code positionné dans le bloc finally
est exécuté après celui se trouvant dans l’ensemble try
/catch
; ensemble qui, lui, ne voit pas son comportement être altéré par rapport aux versions précédentes de PHP.
Et avec un return
au milieu ?
Je disais juste au-dessus que le code se trouvant dans le bloc finally
était exécuté après celui de l’ensemble try
/catch
; une question qui vient presque naturellement à l’esprit, dans ce cas, est “que se passe-t’il si on utilise return
depuis le bloc try
?”
Essayons avec la portion de code suivante :
function fonction_test() {
echo "dans fonction : avant\n";
try {
echo "try\n";
return 123;
}
catch (Exception $e) {
// Pas d'exception lancée dans le bloc try
// => On ne passe pas ici
echo "catch\n";
}
finally {
// On passe dans le bloc finally, même
// s'il y a un return dans le bloc try
echo "finally\n";
}
echo "dans fonction : apres\n";
}
echo "avant\n";
fonction_test();
echo "apres\n";
En exécutant la portion de code reproduite au-dessus, nous obtiendrions la sortie suivante :
avant
dans fonction : avant
try
finally
apres
Autrement dit : le code se trouvant dans le bloc finally
est exécuté après celui se trouvant dans le bloc try
, même si celui-ci contient une instruction return
.
Par acquis de conscience, vérifions ce qu'il en est si le `return` est positionné dans un bloc `catch` :
function fonction_test() {
echo "dans fonction : avant\n";
try {
echo "try : avant throw\n";
throw new Exception("Boum !");
// Une exception a été levée => on ne passe pas ici
echo "try : apres throw";
}
catch (Exception $e) {
// Exception lancée dans le bloc try
// => On passe ici
echo "catch\n";
return 456;
}
finally {
// On passe dans le bloc finally, même
// s'il y a un return dans le bloc try
echo "finally\n";
}
echo "dans fonction : apres\n";
}
echo "avant\n";
fonction_test();
echo "apres\n";
La sortie obtenue avec cette portion de code sera la suivante :
avant
dans fonction : avant
try : avant throw
catch
finally
apres
Autrement dit, ici aussi, le code correspondant au bloc finally
est exécuté après celui correspondant au bloc catch
– et même si celui-ci contient une instruction return
.
En bref, le bloc finally
peut être utilisé pour effectuer des opérations telle que du nettoyage, indépendamment de ce qui est fait en cas de levée – ou non – d’exception ; ce qui correspond à l’objectif pour lequel ce nouveau comportement a été implémenté.
finally
et die
? Et Fatal Error
?
On peut aller un peu plus loin encore, et se demander si le bloc finally
est exécuté dans le cas où l’exécution du bloc try
(même chose pour le bloc catch
) est interrompue par un appel à die()
ou à exit()
:
echo "avant\n";
try {
echo "try\n";
die;
}
catch (Exception $e) {
// Pas d'exception lancée dans le bloc try
// => On ne passe pas ici
echo "catch\n";
}
finally {
echo "finally\n";
}
echo "apres\n";
Si nous exécutons la portion de code reproduite ci-dessus, nous obtiendrons la sortie suivante :
avant
try
Autrement dit, si nous demandons à stopper l’exécution d’un script en appelant die()
ou exit()
, l’exécution de ce script sera effectivement stoppée – et ce même si die()
ou exit()
ont été appelés depuis un bloc try
auquel correspond un bloc finally
: le bloc finally
n’est pas exécuté.
On obtiendrait le même type de comportement si une `Fatal Error` était levée depuis le bloc `try` :
echo "avant\n";
try {
echo "try\n";
echo (object)[]; // Catchable fatal error
}
catch (Exception $e) {
// Pas d'exception lancée dans le bloc try
// => On ne passe pas ici
echo "catch\n";
}
finally {
echo "finally\n";
}
echo "apres\n";
La sortie obtenue en exécutant la portion de code ci-dessous serait la suivante :
avant
try
Catchable fatal error: Object of class stdClass could not be converted to string
in /.../php-5.5/tests/finally/finally-6.php on line 8
En somme, une erreur fatale reste fatale, et est suffisament importante pour prendre le pas sur un éventuel bloc finally
.
Et sans catcher ?
Il est tout à fait possible d’utiliser un couple try
/finally
sans utiliser de bloc catch
au milieu.
Cela peut être utilisé pour garantir qu’une portion de code sera exécutée dans tous les cas ; par exemple, pour exécuter une portion de code à la fin d’une fonction ayant plusieurs instructions return
3 :
function test($param) {
echo "avant try\n";
try {
switch ($param) {
case 0:
return '>> plop';
case 1:
return '>> glop';
default:
return '>> hello';
}
}
finally {
echo "finally\n";
}
}
echo "avant\n";
echo test(mt_rand(0, 5)) . "\n";
echo "apres\n";
Quelque soit l’instruction return
par laquelle cette fonction se terminera, le code positionné dans le bloc finally
sera systématiquement exécuté avant que le contrôle ne soit rendu à l’appelant ; et nous obtiendrons une sortie de ce type :
avant
avant try
finally
>> hello
apres
Note : ce n’est pas parce que c’est possible qu’il faut user et abuser de cette possibilité : niveau lisibilité et compréhension du code, ce n’est pas forcément idéal…
Puisque j'en suis à parler de non-attrapage d'exception : si une exception est levée sans être interceptée, que ce soit parce qu'il n'y a aucun bloc `catch` ou parce qu'il n'y a aucun bloc `catch` correspondant au type d'exception levée, comme ici par exemple :
try {
echo "try\n";
// Exception non catchée
throw new Exception("Ca va faire mal !");
}
catch (RuntimeException $e) {
// Ne catche pas Exception qui est plus générique que RuntimeException
echo "catch\n";
}
finally {
echo "finally\n";
}
La sortie obtenue sera la suivante :
try
finally
Fatal error: Uncaught exception 'Exception' with message 'Ca va faire mal !'
in /.../php-5.5/tests/finally/finally-10.php:8
Stack trace:
#0 {main}
thrown in /.../php-5.5/tests/finally/finally-10.php on line 8
Le bloc finally
est exécuté suite à la levée de l’exception depuis le bloc try
– mais celle-ci n’est interceptée nulle part, et, comme avant, le script se termine en Fatal error
.
Quelques cas marrant avec return
/ continue
/ break
/ goto
Il reste encore plusieurs cas que nous pourrions avoir envie de tester ; en voici quelques-uns.
return
dans le try
et dans le finally
Nous avons vu un peu plus haut que dans le cas où une instruction return
est utilisée dans un bloc try
, le bloc finally
correspondant est tout de même exécuté.
Cherchons un peu plus loin : que se passe-t’il dans le cas où une instruction return
est utilisée dans un bloc try
, et une seconde instruction return
, avec une valeur de retour différente, est utilisée dans le bloc finally
correspondant ?
function test() {
try {
return 'try';
}
finally {
return 'finally';
}
}
echo test();
En exécutant cette portion de code, nous obtiendrons la sortie suivante :
finally
Ainsi, c’est le second return
exécuté, celui positionné dans le bloc finally
, qui a raison ; et qui définit la valeur effectivement retournée par la fonction.
break
/ continue
et try
/ finally
Si nous positionnons une instruction break
dans un bloc try
:
for ($i = 0 ; $i < 5 ; $i++) {
try {
echo "try $i\n";
break;
}
finally {
echo "finally $i\n";
}
}
La sortie obtenue nous montre que le bloc finally
est exécuté – comme nous aurions pu nous y attendre :
try 0
finally 0
Et si nous faisons le même test avec une instruction continue
:
for ($i = 0 ; $i < 5 ; $i++) {
try {
echo "try $i\n";
continue;
}
finally {
echo "finally $i\n";
}
}
La aussi, la sortie nous montre que le bloc finally
est exécuté :
try 0
finally 0
try 1
finally 1
try 2
finally 2
try 3
finally 3
try 4
finally 4
En somme, continue
et break
n’empêchent pas l’exécution d’un bloc finally
.
Et avec goto
?
On peut aussi effectuer le même type de test avec une instruction goto
4 placée dans un bloc try
:
function test() {
try {
echo "try\n";
goto label;
}
finally {
echo "finally\n";
return "123";
}
echo "avant label\n";
label:
echo "label\n";
return "456";
}
echo test() . "\n";
Nous obtiendrons alors la sortie suivante, qui montre une fois de plus que le bloc finally
est exécuté, et qu’il fait disparaitre le goto label;
qui se trouvait dans le bloc try
:
try
finally
123
Note : si on commente le return
du bloc finally
, on obtient bien entendu la sortie suivante, où le goto
a bien été pris en compte :
try
finally
label
456
Notons qu'il n'est pas possible d'utiliser l'instruction `goto` pour sauter hors d'un bloc `finally` :
function test() {
try {
echo "try\n";
}
finally {
echo "finally\n";
goto label;
}
label:
echo "label\n";
}
echo test() . "\n";
La portion de code reproduite ci-dessus, si exécutée, entrainera une Fatal error
:
Fatal error: jump out of a finally block is disallowed
in /.../php-5.5/tests/finally/finally-12.php on line 10
Un mot pour la fin ?
Pour résumer, PHP, probablement en sa prochaine version, implémente enfin le bloc finally
au sein de sa gestion d’exceptions.
Celui-ci a principalement pour rôle de nous permettre d’effectuer, de manière centralisée, des opérations de nettoyage, communes au cas où une exception (quelque soit son type) est levée et à celui où aucune exception ne survient.
Bien sûr, comme avec d’autres constructions du langage, finally
peut mener à du code difficile à comprendre : après tout, ce nouveau mot-clef apporte une manière supplémentaire d’altérer l’exécution d’une portion de code, et rajoute des chemins possibles au sein de celle-ci.
Ici encore, c’est à nous développeurs de prendre soin d’utiliser cette construction de manière réfléchie… et de ne pas abuser des possibilités qu’elle nous offre.
Voir aussi
- RFC: Supports finally keyword
- Bug #32100: Request ‘finally’ support for exceptions : cette fonctionnalité était demandée dès 2005, mais n’avait pas été jugée comme nécessaire / utile. La discussion s’est relancée en 2010, pour finallement arriver à une implémentation en 2012.
- Bug #36779: Request ‘finally’ support for exceptions : une autre demande, qui remonte, elle, à 2006.
L'autre étant les *generators*, dont je parlerai dans un prochain article ; peut-être la semaine prochaine ;-)
Oui, un seul 'l' à [*finalenement*](http://fr.wiktionary.org/wiki/finalement) en français, et deux 'l' à [*finally*](http://en.wiktionary.org/wiki/finally) en anglais.
<br />Et désolé pour le jeu de mots, je n'ai pas pu m'en empêcher -- côté positif, maintenant que c'est fait, j'ai l'esprit libre pour penser à autre chose ^^
*(Oui, cette note de bas de page répête ce que je dis quelques lignes plus bas)*
Ce n'est pas parce que c'est **possible** que vous devez le faire, hein... à vous de juger si ce type d'écriture est lisible, et mérite sa place dans votre code et dans celui de vos applications -- la question peut aussi intéresser vos collègues, d'ailleurs ;-)
Non, [ne lâchez pas les raptors](http://xkcd.com/292/) !