Quelles sont les meilleures pratiques pour intercepter et relancer des exceptions?

156

Les exceptions interceptées doivent-elles être renvoyées directement ou doivent-elles être entourées d'une nouvelle exception?

Autrement dit, dois-je faire ceci:

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

ou ca:

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

Si votre réponse est de lancer directement, veuillez suggérer l'utilisation du chaînage d'exceptions , je ne suis pas en mesure de comprendre un scénario réel où nous utilisons le chaînage d'exceptions.

Rahul Prasad
la source

Réponses:

287

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

"Quelque chose de significatif" peut être l'un de ces éléments:

Gérer l'exception

L'action significative la plus évidente est de gérer 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 gérer correctement une exception dans un contexte spécifique; peut-être manquez-vous d'informations sur la «vue d'ensemble», mais vous voulez enregistrer l'échec le plus près possible du point où il s'est produit. Dans ce cas, vous souhaiterez peut-être attraper, enregistrer et 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 géré au niveau supérieur. Dans les versions antérieures de PHP, cela serait implémenté comme

$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 le finallymot - clé, donc pour les scénarios de nettoyage, il existe maintenant une autre façon d'aborder cela. Si le code de nettoyage doit s'exécuter quoi qu'il arrive (c'est-à-dire à la fois en cas d'erreur et de succès), il est maintenant possible de le faire tout en permettant de manière transparente à toutes les exceptions levées de se propager:

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

Abstraction d'erreur (avec chaînage d'exceptions)

Un troisième cas est celui où vous souhaitez regrouper logiquement de nombreuses pannes possibles sous un plus grand parapluie. 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 Componentsachent qu'il est implémenté à l'aide d'une connexion à une base de données (peut-être souhaitez-vous garder vos options ouvertes et utiliser le stockage basé sur des fichiers à l'avenir). Donc, votre spécification pour Componentdirait que "dans le cas d'un échec d'initialisation, ComponentInitExceptionsera jeté". Cela permet aux consommateurs d' Componentintercepter les exceptions du type attendu tout en permettant au code de débogage d'accéder à tous les détails (dépendants de l'implémentation) .

Fournir un contexte plus riche (avec chaînage d'exceptions)

Enfin, dans certains cas, vous souhaiterez peut-être fournir plus de contexte pour l'exception. Dans ce cas, il est judicieux d'encapsuler l'exception dans une autre qui contient plus d'informations sur ce que vous tentiez 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 à celui ci-dessus (et l'exemple n'est probablement pas le meilleur que l'on puisse trouver), mais il illustre l'intérêt de fournir plus 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 encapsulées (dont il pourrait y avoir plus d'un niveau si l'exemple était beaucoup plus compliqué).

La valeur de cette opération est illustrée si vous pensez à un scénario où, par exemple, la création d'un UserProfileobjet entraîne la copie de fichiers car le profil utilisateur est stocké dans des fichiers et prend en charge la sémantique des transactions: vous pouvez «annuler» les modifications car elles ne sont effectuées que sur un copie du profil jusqu'à ce que vous vous engagiez.

Dans ce cas, si vous l'avez fait

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

et en conséquence a attrapé une erreur d'exception «Le répertoire cible n'a pas pu être créé», vous auriez le droit d'être confus. Emballer cette exception "principale" dans des couches d'autres exceptions qui fournissent du 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éé").

Jon
la source
Je suis d'accord uniquement avec les 2 dernières raisons: 1 / gérer l'exception: vous ne devriez pas le faire à ce niveau, 2 / journalisation ou nettoyage: utilisez enfin et enregistrez l'exception au-dessus de votre datalayer
remi bourgarel
1
@remi: sauf que PHP ne supporte pas la finallyconstruction (pas encore du moins) ... Donc c'est
fini
@remibourgarel: 1: C'était juste un exemple. Bien sûr, vous ne devriez pas le faire à ce niveau, mais la réponse est assez longue comme ça. 2: Comme le dit @ircmaxell, il n'y a pas finallyde PHP.
Jon
3
Enfin, PHP 5.5 est désormais implémenté.
OCDev
12
Il y a une raison pour laquelle je pense que vous avez manqué votre liste ici - vous ne pourrez peut-être pas dire si vous pouvez gérer une exception tant que vous ne l'avez pas détectée et que vous n'avez pas eu l'occasion de l'inspecter. Par exemple, un wrapper pour une API de niveau inférieur qui utilise des codes d'erreur (et en a des millions) peut avoir une seule classe d'exception dont il lève une instance pour toute erreur, avec une error_codepropriété qui peut être vérifiée pour obtenir l'erreur sous-jacente code. Si vous ne pouvez gérer de manière significative que certaines de ces erreurs, vous voudrez probablement attraper, inspecter et, si vous ne pouvez pas gérer l'erreur, renvoyer.
Mark Amery le
37

Eh bien, il s'agit de maintenir l'abstraction. Je suggère donc d'utiliser le chaînage d'exceptions pour lancer directement. Quant à savoir pourquoi, laissez-moi vous expliquer le concept d' abstractions qui fuient

Disons que vous construisez un modèle. Le modèle est censé éliminer toutes les données de persistance et de validation du reste de l'application. Alors maintenant, que se passe-t-il lorsque vous obtenez une erreur de base de données? Si vous relancez le DatabaseQueryException, vous perdez l'abstraction. Pour comprendre pourquoi, pensez à l'abstraction pendant une seconde. Vous ne vous souciez pas de la façon dont le modèle stocke les données, mais simplement de ce qu'il fait. De même, vous ne vous souciez pas exactement de ce qui n'a pas fonctionné dans les systèmes sous-jacents du modèle, mais simplement du fait que vous savez que quelque chose s'est mal passé, et à peu près ce qui n'a pas fonctionné.

Ainsi, en renvoyant l'exception DatabaseQueryException, vous divulguez l'abstraction et exigez que le code appelant comprenne la sémantique de ce qui se passe sous le modèle. Au lieu de cela, créez un générique ModelStorageExceptionet enveloppez le capturé à l' DatabaseQueryExceptionintérieur de celui-ci. De cette façon, votre code d'appel peut toujours essayer de traiter l'erreur sémantiquement, mais la technologie sous-jacente du modèle n'a pas d'importance, car vous n'exposez que les erreurs de cette couche d'abstraction. Mieux encore, puisque vous avez encapsulé l'exception, si elle bouillonne complètement et doit être journalisée, vous pouvez tracer jusqu'à l'exception racine lancée (parcourir la chaîne) afin que vous ayez toujours toutes les informations de débogage dont vous avez besoin!

Ne vous contentez pas d'attraper et de renvoyer la même exception à moins que vous n'ayez besoin de faire un post-traitement. Mais un bloc comme ça ne } catch (Exception $e) { throw $e; }sert à rien. Mais vous pouvez reconditionner les exceptions pour un gain d'abstraction significatif.

ircmaxell
la source
2
Très bonne réponse. Il semble que beaucoup de gens autour de Stack Overflow (en fonction des réponses, etc.) les utilisent mal.
James
8

IMHO, attraper une exception pour simplement la relancer est inutile . Dans ce cas, ne l'attrapez pas et laissez les méthodes appelées plus tôt le gérer ( c'est -à-dire les méthodes «supérieures» dans la pile d'appels) .

Si vous la relancez, enchaîner l'exception capturée dans la nouvelle que vous lancerez est certainement une bonne pratique, car elle conservera les informations contenues dans l'exception capturée. Cependant, le renvoyer n'est utile que si vous ajoutez des informations ou gérez quelque chose à l'exception interceptée, que ce soit un contexte, des valeurs, une journalisation, la libération de ressources, peu importe.

Une façon d'ajouter des informations est d'étendre la Exceptionclasse, d'avoir des exceptions comme NullParameterException, DatabaseException, etc. Plus sur, cela permet au developpeur attraper seulement quelques exceptions près qu'il peut gérer. Par exemple, on peut attraper seulement DatabaseExceptionet essayer de résoudre ce qui a causé le Exception, comme la reconnexion à la base de données.

Clément Herreman
la source
2
Ce n'est pas inutile, il y a des moments où vous avez besoin de faire quelque chose sur une exception, par exemple dans la fonction qui la lance, puis de la relancer pour laisser un catch plus haut faire autre chose. Dans l'un des projets sur lesquels je travaille, nous détectons parfois une exception dans une méthode d'action, affichons un avis convivial à l'utilisateur, puis le relançons afin qu'un bloc try catch plus loin dans le code puisse le rattraper pour enregistrer l'erreur dans un journal.
MitMaro
1
Donc, comme je l'ai dit, vous ajoutez des informations à l'exception (affichage d'un avis, enregistrement). Vous ne vous contentez pas de le relancer comme dans l'exemple du PO.
Clement Herreman
2
Eh bien, vous pouvez simplement le renvoyer 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 au monde, mais ce n'est pas horrible
ircmaxell
2
@ircmaxell D'accord, modifié pour refléter qu'il n'est inutile que si vous ne faites rien d'autre que de le relancer
Clement Herreman
1
L'important est que vous perdiez le fichier et / ou les informations de ligne de l'endroit où l'exception avait été initialement levée en la relançant. Il est donc généralement préférable d'en trotter un nouveau et de transmettre l'ancien, comme dans le deuxième exemple de la question. Sinon, il pointera simplement vers le bloc catch, vous laissant deviner quel a été le problème réel.
DanMan
2

Vous devez jeter un œil aux meilleures pratiques d'exception en PHP 5.3

La gestion des exceptions en PHP n'est en aucun cas une nouvelle fonctionnalité. Dans le lien suivant, vous verrez deux nouvelles fonctionnalités de PHP 5.3 basées sur les exceptions. Le premier est les exceptions imbriquées et le second est un nouvel ensemble de types d'exceptions offerts par l'extension SPL (qui est maintenant une extension principale 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

HMagdy
la source
1

Vous y pensez généralement de cette façon.

Une classe peut lever de nombreux types d'exceptions qui ne correspondent pas. Vous créez donc une classe d'exception pour cette classe ou ce type de classe et la lancez.

Ainsi, le code qui utilise la classe n'a qu'à attraper un type d'exception.

Ólafur Waage
la source
1
Hey pouvez-vous plz fournir plus de détails ou un lien où je peux en savoir plus sur cette approche.
Rahul Prasad