197 votes

Quelles sont les meilleures pratiques pour attraper et relancer les exceptions ?

Les exceptions relevées doivent-elles être rejetées directement, ou doivent-elles être enveloppées dans une nouvelle exception ?

C'est-à-dire, est-ce que je dois faire ça :

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw $e;
}

ou ceci :

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw new Exception("Exception Message", 1, $e);
}

Si votre réponse est lancer directement veuillez suggérer l'utilisation de enchaînement d'exceptions Je ne suis pas en mesure de comprendre un scénario réel dans lequel nous utilisons le chaînage d'exceptions.

346voto

Jon Points 194296

Vous ne devriez pas attraper l'exception à moins que vous n'ayez l'intention de faire quelque chose de significatif .

"Quelque chose de significatif" pourrait être l'un d'entre eux :

Traitement de l'exception

L'action significative la plus évidente consiste à traiter l'exception, par exemple en affichant un message d'erreur et en abandonnant l'opération :

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    echo "Error while connecting to database!";
    die;
}

Journalisation ou nettoyage partiel

Parfois, vous ne savez pas comment traiter correctement une exception dans un contexte spécifique ; peut-être manquez-vous d'informations sur la "situation globale", mais vous voulez enregistrer l'échec aussi près que possible du point où il s'est produit. Dans ce cas, il est préférable d'attraper, d'enregistrer et de relancer :

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    logException($e); // does something
    throw $e;
}

Un scénario connexe est celui où vous êtes au bon endroit pour effectuer un nettoyage de l'opération qui a échoué, mais pas pour décider comment l'échec doit être traité au niveau supérieur. Dans les versions antérieures de PHP, ceci serait implémenté comme suit

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
catch (Exception $e) {
    $connect->disconnect(); // we don't want to keep the connection open anymore
    throw $e; // but we also don't know how to respond to the failure
}

PHP 5.5 a introduit la fonction finally Ainsi, pour les scénarios de nettoyage, il existe désormais une autre façon d'aborder la question. Si le code de nettoyage doit s'exécuter quoi qu'il arrive (c'est-à-dire aussi bien en cas d'erreur qu'en cas de succès), il est maintenant possible de le faire tout en permettant de manière transparente la propagation des exceptions levées :

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
finally {
    $connect->disconnect(); // no matter what
}

Abstraction des erreurs (avec chaînage des exceptions)

Le troisième cas est celui où l'on souhaite regrouper logiquement plusieurs défaillances possibles sous un même chapeau. Un exemple de regroupement logique :

class ComponentInitException extends Exception {
    // public constructors etc as in Exception
}

class Component {
    public function __construct() {
        try {
            $connect = new CONNECT($db, $user, $password, $driver, $host);
        }
        catch (Exception $e) {
            throw new ComponentInitException($e->getMessage(), $e->getCode(), $e);
        }
    }
}

Dans ce cas, vous ne voulez pas que les utilisateurs de Component de savoir qu'il est mis en œuvre à l'aide d'une connexion à une base de données (vous souhaitez peut-être garder vos options ouvertes et utiliser un stockage basé sur des fichiers à l'avenir). Ainsi, votre spécification pour Component dirait que "dans le cas d'un échec de l'initialisation, ComponentInitException seront lancés". Cela permet aux consommateurs de Component pour attraper les exceptions du type attendu tout en permettant au code de débogage d'accéder à tous les détails (dépendant de l'implémentation). .

Fournir un contexte plus riche (avec le chaînage des exceptions)

Enfin, il y a des cas où vous pouvez vouloir fournir plus de contexte pour l'exception. Dans ce cas, il est logique d'envelopper l'exception dans une autre exception qui contient plus d'informations sur ce que vous essayiez de faire lorsque l'erreur s'est produite. Par exemple :

class FileOperation {
    public static function copyFiles() {
        try {
            $copier = new FileCopier(); // the constructor may throw

            // this may throw if the files do no not exist
            $copier->ensureSourceFilesExist();

            // this may throw if the directory cannot be created
            $copier->createTargetDirectory();

            // this may throw if copying a file fails
            $copier->performCopy();
        }
        catch (Exception $e) {
            throw new Exception("Could not perform copy operation.", 0, $e);
        }
    }
}

Ce cas est similaire au précédent (et l'exemple n'est probablement pas le meilleur que l'on puisse trouver), mais il illustre l'intérêt de fournir davantage de contexte : si une exception est levée, cela nous indique que la copie du fichier a échoué. Mais pourquoi a-t-il échoué ? Ces informations sont fournies dans les exceptions enveloppées (dont il pourrait y avoir plus d'un niveau si l'exemple était beaucoup plus compliqué).

L'intérêt de cette démarche est illustré si vous pensez à un scénario dans lequel, par exemple, la création d'une UserProfile provoque la copie de fichiers parce que le profil de l'utilisateur est stocké dans des fichiers et qu'il prend en charge la sémantique des transactions : vous pouvez "annuler" les modifications car elles ne sont effectuées que sur une copie du profil jusqu'à ce que vous les validiez.

Dans ce cas, si vous avez

try {
    $profile = UserProfile::getInstance();
}

et que, par conséquent, vous obteniez une exception d'erreur "Le répertoire cible n'a pas pu être créé", vous auriez le droit d'être confus. Envelopper cette exception "principale" dans des couches d'autres exceptions qui fournissent un contexte rendra l'erreur beaucoup plus facile à traiter ("La création de la copie du profil a échoué" -> "L'opération de copie du fichier a échoué" -> "Le répertoire cible n'a pas pu être créé").

0 votes

Je ne suis d'accord qu'avec les deux dernières raisons : 1/ traitement de l'exception : vous ne devriez pas le faire à ce niveau, 2/ enregistrement ou nettoyage : utilisez finally et enregistrez l'exception au-dessus de votre couche de données.

1 votes

@remi : sauf que PHP ne supporte pas l'option finally construire (pas encore du moins)... Donc, c'est exclu, ce qui signifie que nous devons recourir à des choses sales comme ceci...

0 votes

@remibourgarel : 1 : C'était juste un exemple. Bien sûr, il ne faut pas le faire à ce niveau, mais la réponse est assez longue comme ça. 2 : Comme @ircmaxell l'a dit, il n'y a pas d'autre solution. finally en PHP.

45voto

ircmaxell Points 74865

Eh bien, il s'agit de maintenir l'abstraction. Donc je suggère d'utiliser le chaînage d'exceptions pour lancer directement. Pour ce qui est du pourquoi, laissez-moi vous expliquer le concept de abstractions non étanches

Disons que vous construisez un modèle. Le modèle est censé faire abstraction de toute la persistance et de la validation des données du reste de l'application. Que se passe-t-il alors lorsque vous obtenez une erreur de base de données ? Si vous relancez la méthode DatabaseQueryException vous faites fuir l'abstraction. Pour comprendre pourquoi, pensez à l'abstraction pendant une seconde. Vous ne vous souciez pas comment le modèle stocke les données, juste qu'il le fait. De même, vous ne vous souciez pas de savoir exactement ce qui s'est passé dans les systèmes sous-jacents du modèle, mais simplement de savoir que quelque chose s'est mal passé, et approximativement ce qui s'est mal passé.

Ainsi, en relançant l'exception DatabaseQueryException, vous laissez échapper l'abstraction et vous demandez au code appelant de comprendre la sémantique de ce qui se passe sous le modèle. Au lieu de cela, créez une méthode générique ModelStorageException et d'envelopper la prise DatabaseQueryException à l'intérieur de celui-ci. De cette façon, votre code appelant peut toujours essayer de traiter l'erreur sémantiquement, mais la technologie sous-jacente du modèle n'a pas d'importance puisque vous n'exposez les erreurs qu'à partir de cette couche d'abstraction. Mieux encore, puisque vous avez enveloppé l'exception, si elle remonte et doit être consignée, vous pouvez remonter jusqu'à l'exception racine (en remontant la chaîne) afin de disposer de toutes les informations de débogage dont vous avez besoin !

Ne vous contentez pas d'attraper et de relancer la même exception, sauf si vous devez effectuer un traitement ultérieur. Mais un bloc comme } catch (Exception $e) { throw $e; } est inutile. Mais vous pouvez ré-envelopper les exceptions pour un gain d'abstraction significatif.

3 votes

Excellente réponse. Il semble que beaucoup de personnes sur Stack Overflow (d'après les réponses, etc.) les utilisent mal.

7voto

Clement Herreman Points 5642

IMHO, attraper une exception pour simplement la rejeter est inutile . Dans ce cas, il suffit de ne pas l'attraper et de laisser les méthodes appelées précédemment s'en charger. (c'est-à-dire les méthodes qui sont "supérieures" dans la pile d'appels) .

Si vous la relancez, enchaîner l'exception levée dans la nouvelle exception que vous allez lancer est certainement une bonne pratique, car cela permettra de conserver les informations que l'exception levée contient. Cependant, le renvoi de l'exception n'est utile que si vous ajouter des informations ou traiter quelque chose à l'exception détectée, qu'il s'agisse d'un contexte, de valeurs, d'enregistrement, de libération de ressources, etc.

Un moyen d'ajouter des informations est d'étendre la fonction Exception pour avoir des exceptions comme NullParameterException , DatabaseException etc. De plus, cela permet au développeur de n'attraper que certaines exceptions qu'il peut gérer. Par exemple, on peut attraper seulement DatabaseException et essayer de résoudre ce qui a causé le Exception comme se reconnecter à la base de données.

4 votes

Il n'est pas inutile, il y a des moments où vous devez faire quelque chose sur une exception, par exemple dans la fonction qui la lance, puis la relancer pour permettre à une fonction supérieure de faire autre chose. Dans l'un des projets sur lesquels je travaille, il nous arrive d'attraper une exception dans une méthode d'action, d'afficher une notification amicale à l'utilisateur, puis de la relancer afin qu'un bloc try catch situé plus loin dans le code puisse l'attraper à nouveau pour enregistrer l'erreur dans un journal.

1 votes

Donc, comme je l'ai dit, vous ajoutez des informations à l'exception (affichage d'un avis, enregistrement). Vous ne faites pas il suffit de le rejeter comme dans l'exemple de l'OP.

2 votes

Vous pouvez simplement le relancer si vous avez besoin de fermer des ressources, mais que vous n'avez aucune information supplémentaire à ajouter. Je suis d'accord que ce n'est pas la chose la plus propre du monde, mais ce n'est pas horribles

2voto

Ólafur Waage Points 40104

On y pense généralement de cette façon.

Une classe peut lancer de nombreux types d'exceptions qui ne correspondent pas. Il faut donc créer une classe d'exceptions pour cette classe ou ce type de classe et la lancer.

Ainsi, le code qui utilise la classe ne doit attraper qu'un seul type d'exception.

2 votes

Pouvez-vous me donner plus de détails ou un lien où je peux en savoir plus sur cette approche ?

2voto

Hassan Magdy Points 572

Vous devez jeter un coup d'oeil sur Meilleures pratiques en matière d'exceptions en PHP 5.3

La gestion des exceptions en PHP n'est pas une nouveauté, loin de là. Dans le lien suivant, vous verrez deux nouvelles fonctionnalités de PHP 5.3 basées sur les exceptions. La première est l'imbrication des exceptions et la seconde est un nouveau jeu de types d'exceptions offert par l'extension SPL (qui est maintenant une extension de base du runtime PHP). Ces deux nouvelles fonctionnalités ont trouvé leur place dans le livre des meilleures pratiques et méritent d'être examinées en détail.

http://ralphschindler.com/2010/09/15/exception-best-practices-in-php-5-3

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X