En route vers PHP 5.5 : Bloc finally pour les exceptions

23 octobre 2012php, php-5.5
 Cet article a été rédigé il y a plusieurs années et peut ne plus être tout à fait à jour…

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 return3 :

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 goto4 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

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 !