backend / php-laravel
Gestion des erreurs et exceptions
Explication
Ce que vous allez apprendre
- Créer une hiérarchie d'exceptions métier personnalisées plutôt que de tout attraper génériquement
- Utiliser
try/catch/finallyen comprenant précisément quand chaque bloc s'exécute - Distinguer
ExceptionetErrorgrâce à l'interface communeThrowable - Chaîner des exceptions pour préserver la cause originale d'une erreur lors du débogage
- Mettre en place un gestionnaire d'erreurs global comme dernier filet de sécurité
Dans quel contexte ?
Un système de commande e-commerce doit rejeter proprement une commande dont le stock est insuffisant, tout en distinguant ce cas métier prévisible d'une vraie panne (base de données injoignable, fichier de configuration corrompu). Sans une hiérarchie d'exceptions claire, le code applicatif finit par attraper des erreurs génériques avec des if fragiles sur le texte du message — une pratique qui casse au moindre changement de formulation.
D'abord, pourquoi ne pas se contenter de la classe Exception générique ?
Si toutes les erreurs sont de simples \Exception, il devient impossible de réagir différemment selon le type de problème. Créer des classes dédiées comme StockInsuffisantException qui héritent d'Exception permet d'attraper précisément ce cas, avec ses propres données utiles ($produit, $demande, $disponible), sans toucher au reste du code d'erreur.
Cette hiérarchie n'est pas juste esthétique : elle change concrètement comment le code réagit, en autorisant un catch (StockInsuffisantException $e) très ciblé avant un catch plus générique en filet de sécurité.
Prérequis
Une bonne maîtrise de la POO (héritage, constructeurs) vue en leçon 4 est indispensable ici : une exception personnalisée est avant tout une classe comme une autre.
Ensuite, un ordre d'exécution qui surprend parfois
Le bloc finally s'exécute systématiquement, que le try ait réussi, échoué, ou même contienne un return. C'est l'endroit idéal pour libérer des ressources (fermer une connexion, un fichier) sans dupliquer ce code dans chaque branche possible.
| Bloc | S'exécute quand ? | Usage typique |
|---|---|---|
try | Toujours en premier | Code potentiellement risqué |
catch | Si une exception du bon type est levée | Gestion de l'erreur |
finally | Toujours, quel que soit le résultat | Nettoyage de ressources |
Il reste une distinction subtile mais importante : Exception contre Error
PHP distingue les Exception (erreurs métier attendues, comme un stock insuffisant) des Error (erreurs plus fondamentales du langage, comme un appel de méthode inexistante). Avant PHP 7, une Error fatale plantait le script sans possibilité de la rattraper ; aujourd'hui, l'interface commune Throwable permet d'attraper les deux avec un seul catch (\Throwable $e).
Piège fréquent
Un catch (\Exception $e) ne rattrapera PAS une TypeError ou une DivisionByZeroError, qui héritent d'Error et non d'Exception. Si tu veux vraiment tout intercepter en dernier recours, utilise \Throwable, l'interface parente commune aux deux.
Maintenant, ne jamais perdre la trace du problème d'origine
Quand une exception de bas niveau (une erreur JSON, par exemple) doit être traduite en une exception plus parlante pour l'appelant, il ne faut pas perdre l'information d'origine. Le paramètre previous: du constructeur d'exception chaîne les deux : getPrevious() permet de remonter toute la chaîne de causes lors du débogage, même en production.
Astuce
set_error_handler() transforme les warnings et notices PHP classiques (qui, historiquement, ne cassent pas l'exécution) en véritables exceptions catchables. C'est une pratique recommandée en début de projet : elle empêche des erreurs silencieuses de passer inaperçues jusqu'en production.
Une fois la gestion des erreurs bien maîtrisée, il devient temps de structurer un vrai projet PHP avec des dépendances externes : direction Composer et l'autoloading, sujet de la prochaine leçon.
Commandes & code
Gestion des erreurs et exceptions
<?php
declare(strict_types=1);
// Hiérarchie d'exceptions personnalisée : facilite le catch ciblé
class DomaineException extends \Exception {}
class StockInsuffisantException extends DomaineException {
public function __construct(
public readonly string $produit,
public readonly int $demande,
public readonly int $disponible
) {
parent::__construct(
"Stock insuffisant pour {$produit}: demandé {$demande}, disponible {$disponible}"
);
}
}
function commander(string $produit, int $quantite, int $stockDisponible): void {
if ($quantite > $stockDisponible) {
throw new StockInsuffisantException($produit, $quantite, $stockDisponible);
}
echo "Commande validée pour $quantite x $produit
";
}
// try / catch / finally : finally s'exécute toujours (succès, erreur, ou return)
try {
commander("Clavier", 10, 3);
} catch (StockInsuffisantException $e) {
echo "Erreur métier: {$e->getMessage()}
";
echo "Manque: " . ($e->demande - $e->disponible) . " unités
";
} catch (\Throwable $e) {
// Throwable capture Error ET Exception (fatal errors compris depuis PHP 7)
echo "Erreur inattendue: {$e->getMessage()}
";
} finally {
echo "Traitement de la commande terminé
";
}
// Catch multiple avec union de types (PHP 8+)
function parserEntier(string $valeur): int {
try {
return (int) filter_var($valeur, FILTER_VALIDATE_INT, ['options' => ['default' => null]] + ['flags' => FILTER_NULL_ON_FAILURE])
?? throw new \ValueError("Valeur non entière: $valeur");
} catch (\ValueError | \TypeError $e) {
error_log($e->getMessage());
return 0;
}
}
// Exceptions chaînées : préserver la cause originale
function chargerConfiguration(string $chemin): array {
try {
$contenu = file_get_contents($chemin);
if ($contenu === false) {
throw new \RuntimeException("Impossible de lire $chemin");
}
return json_decode($contenu, true, flags: JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
// "previous" garde la trace de l'exception d'origine pour le debugging
throw new \RuntimeException("Configuration JSON invalide dans $chemin", previous: $e);
}
}
// set_error_handler : convertir les warnings/notices en exceptions (bonne pratique)
set_error_handler(function (int $niveau, string $message, string $fichier, int $ligne) {
throw new \ErrorException($message, 0, $niveau, $fichier, $ligne);
});
// Gestionnaire global pour les exceptions non attrapées (dernier filet)
set_exception_handler(function (\Throwable $e) {
error_log("[FATAL] {$e->getMessage()} dans {$e->getFile()}:{$e->getLine()}");
http_response_code(500);
echo json_encode(["erreur" => "Une erreur interne est survenue"]);
});Résumé
- Créer des exceptions métier spécifiques (
StockInsuffisantException) plutôt que des\Exceptiongénériques. finallys'exécute toujours, utile pour libérer des ressources.previous:chaîne les exceptions et préserve la cause racine pour le debug.set_error_handlertransforme les erreurs PHP classiques en exceptions catchables.
Exercices pratiques
Mission : distinguer une rupture de stock d'une vraie panne
Objectif : Construire une hierarchie d'exceptions metier et un flux try/catch/finally qui reagit differemment selon le type d'erreur.
Contexte
Le module de commande e-commerce attrape actuellement tout avec un seul catch (\Exception $e) generique, puis inspecte le texte du message avec str_contains($e->getMessage(), 'stock') pour decider s'il doit afficher un message convivial au client ou alerter l'equipe technique. Un changement de formulation du message a recemment casse cette detection en silence : plus aucune alerte ne partait pour les vraies pannes de connexion a la base de donnees.