Gestion des Incidents
Présentation
Lorsqu'un script PHP s'exécute, tout ne se passe pas toujours comme prévu. Une donnée peut manquer, un fichier peut être introuvable, une opération peut échouer, ou le programme peut rencontrer un problème inattendu. Dans ce genre de situation, il faut décider comment réagir proprement.
Tous les problèmes ne se gèrent pas de la même manière. Certaines situations sont normales et prévisibles. Par exemple, un champ de formulaire peut être vide, un mot de passe peut être trop court, ou une valeur peut ne pas respecter le format attendu. Dans ce cas, l'utilisation d'une structure conditionnelle comme if est suffisante.
Cependant, d'autres problèmes peuvent apparaitre pendant l'exécution du script. Par exemple, une lecture de fichier peut échouer, une conversion peut échouer, ou un service externe peut ne pas répondre correctement. Dans ce cas, PHP propose d'autres mécanismes pour signaler le problème et tenter de le gérer plus proprement.
Dans ce chapitre, nous allons donc apprendre à distinguer les cas que l'on peut vérifier simplement avec une condition, et les incidents qui demandent un mécanisme plus structuré. Nous verrons comment signaler un incident, comment le récupérer, puis comment organiser cette gestion de manière plus propre dans une application.
Nous verrons aussi qu'il n'y a pas une seule façon de réagir. En phase de développement, il est souvent utile d'afficher davantage de détails pour comprendre ce qui ne va pas. En production, il vaut mieux éviter d'exposer ces informations à l'utilisateur et enregistrer l'incident dans un fichier de log.
Voici quelques objectifs importants de ce chapitre :
- Distinguer un cas normal d'un vrai incident
- Choisir le bon outil selon la situation
- Éviter les arrêts brutaux quand une réaction plus propre est possible
- Rendre le code plus lisible en séparant le déroulement normal et la gestion des incidents
- Prévoir un comportement adapté en développement et en production
Lever une Exception
En PHP, une exception permet de signaler explicitement qu'une opération a échoué et que le code courant ne peut pas poursuivre son travail normalement. Pour cela, on utilise l'instruction throw.
Lever une exception ne signifie pas forcément que le programme entier doit s'arrêter. Cela signifie d'abord que le code situé à cet endroit précis ne peut pas continuer normalement. Ensuite, deux cas sont possibles. Soit l'exception est capturée plus loin avec un bloc catch, soit elle ne l'est pas et le script s'interrompt.
Une exception peut être utile lorsque le problème rencontré n'est pas un simple cas attendu du métier. Par exemple, si une fonction reçoit un chemin vers un fichier JSON censé exister, mais que le fichier est absent ou illisible, lever une exception peut être une bonne manière de signaler clairement l'échec à l'appelant.
<?php
// Lever une exception avec "throw" en précisant un message destiné au développeur.
throw new Exception('Ceci est une exception de test');
// Ce code ne sera jamais exécuté.
echo 'Ce message ne sera jamais affiché.';
?>
Dans cet exemple, l'exécution s'arrête immédiatement au moment du throw. La ligne située après n'est donc jamais atteinte.
Il faut toutefois éviter de lever des exceptions pour tout et n'importe quoi. Si la situation est simplement attendue et facile à tester avant l'action, une structure conditionnelle reste généralement plus claire. Lever une exception a davantage de sens lorsqu'on veut signaler un échec réel, inattendu pour le code appelant, ou lorsqu'on veut forcer la remontée de l'incident vers un niveau plus approprié.
Capturer les Exceptions
Lorsqu'une exception est levée, PHP arrête immédiatement le bloc courant et cherche un catch compatible. Pour cela, on place le code risqué dans un bloc try et on prévoit un ou plusieurs blocs catch capables de réagir si un incident survient.
Ce mécanisme permet notamment de :
- journaliser un incident technique destiné au développeur ou à l'administrateur ;
- traduire un incident technique en message plus compréhensible pour l'utilisateur ;
- tenter une stratégie de secours ;
- laisser le script poursuivre plus loin, à condition que cela ait encore du sens.
Voici la structure générale :
<?php
try
{
// Code susceptible de lever une exception ou une erreur capturable.
}
catch (Exception $e)
{
// Code exécuté si une exception de type Exception est levée.
}
?>
Dans l'exemple suivant, une exception est levée à l'intérieur du bloc try. Le bloc catch l'intercepte, affiche un message, puis le script continue après l'ensemble try/catch.
<?php
try
{
throw new Exception('Ceci est une exception de test');
// Ce code ne sera jamais exécuté,
// car l'exception interrompt immédiatement le bloc try.
echo 'Ce message ne sera jamais affiché.';
}
catch (Exception $e)
{
echo 'Une erreur est survenue : ' . $e->getMessage();
}
echo 'Exécuter la suite du script...';
?>
Ici, le message d'erreur est affiché par le bloc catch, puis le script poursuit son exécution après ce bloc. Cela ne signifie pas qu'il faut toujours continuer. Il faut continuer seulement si l'état du programme reste cohérent.
La méthode getMessage()
Dans l'instruction $e->getMessage(), on utilise une méthode. Une méthode ressemble à une fonction, mais elle est liée à un objet. Ici, $e est un objet représentant l'incident capturé.
La syntaxe -> permet d'accéder à une méthode ou à une propriété de cet objet. Avec getMessage(), on demande simplement à l'objet de nous renvoyer le message qu'il contient.
Quand utiliser try/catch et quand utiliser une structure conditionnelle ?
Une structure conditionnelle sert à gérer une situation normale, attendue et testable avant l'action. Un mécanisme try/catch sert surtout à réagir à un échec signalé pendant l'exécution.
Exemple adapté à une structure conditionnelle
<?php
$email = trim($_POST['email'] ?? '');
if ($email === '')
{
echo 'Veuillez saisir une adresse email.';
}
else
{
echo 'Le champ email a bien été rempli.';
}
?>
Ici, il n'y a pas d'incident technique. Le champ vide fait partie des cas normaux qu'une application doit savoir gérer. Il est donc inutile de lever une exception pour cela.
Exemple d'appel avec try/catch
<?php
function lireFichierJson(string $chemin): array
{
// Si le fichier n'existe pas, lever une exception.
if (!file_exists($chemin))
{
throw new Exception('Le fichier JSON demandé est introuvable.');
}
$contenu = file_get_contents($chemin);
// Si le contenu du fichier est illisible, lever une exception.
if ($contenu === false)
{
throw new Exception('Impossible de lire le fichier JSON.');
}
$donnees = json_decode($contenu, true);
// Si le Json est mal formaté ou qu'il s'agit d'un autre format, lever une exception.
if (json_last_error() !== JSON_ERROR_NONE)
{
throw new Exception('Le contenu du fichier JSON est invalide.');
}
return $donnees;
}
try
{
// Tentative de lecture du fichier utilisateurs.json
$utilisateurs = lireFichierJson(__DIR__ . '/stockage/utilisateurs.json');
// La suite du code du bloc try ne sera exécuté
// que si la fonction lireFichierJson() n'a pas rencontré d'erreurs.
echo '<pre>';
print_r($utilisateurs);
echo '</pre>';
}
catch (Exception $e)
{
echo 'Incident détecté : ' . $e->getMessage();
}
echo 'Le script continue après le bloc try/catch.';
?>
Dans cet exemple, la fonction lireFichierJson() se contente de signaler le problème avec throw. Elle ne décide pas elle-même comment réagir.
C'est le code appelant qui place un bloc try/catch autour de l'appel à la fonction. Si tout se passe bien, les données sont affichées. Si un problème survient, l'exception est interceptée dans le catch et un message adapté peut être affiché.
Cet exemple montre donc une idée importante. Une fonction peut détecter un incident et le signaler, sans le gérer elle-même. Le traitement de l'incident peut être placé plus haut, à un endroit où l'on sait réellement quoi faire.
Prenons un autre exemple :
- Contexte
- Une fonction A est appelée depuis un bloc try.
- La fonction A appelle une fonction B, qui appelle à son tour une fonction C.
- Dans la fonction C, une exception est levée avec throw.
- Étapes
- Au moment où l'exception est levée dans la fonction C, PHP interrompt immédiatement l'exécution normale à cet endroit.
- PHP cherche alors un bloc try en cours, associé à un catch capable d'intercepter cette exception.
- Si aucun mécanisme adapté n'est trouvé dans la fonction C, PHP remonte dans la fonction appelante B et effectue la même recherche.
- Si rien n'est trouvé non plus dans B, PHP remonte encore jusqu'à A, puis éventuellement jusqu'au code qui a appelé A.
- Si A a été appelée depuis un bloc try possédant un catch compatible, l'exception sera interceptée à cet endroit.
- Si aucun bloc adapté n'est trouvé pendant cette remontée, l'exception reste non interceptée et le script s'interrompt.
Exercices - Partie 01
Exo - Gestion des incidents 01-01 : Lever une exception avec l'instruction throw et observer le comportement du script
- Objectif : Comprendre comment une exception non interceptée interrompt le script et empêche l'exécution des instructions suivantes.
- Instructions :
- Initialiser le projet :
- Créer un dossier nommé exo-gestion-des-incidents-01.
- Créer un fichier nommé exo-01-01.php à l'intérieur de ce dossier.
- Dans le fichier exo-01-01.php, lever une exception avec throw et lui passer un message destiné aux développeurs.
- Juste après cette ligne, ajouter une instruction echo pour afficher le texte : "Le code situé après la levée d'exception..."
- Exécuter le script dans votre navigateur.
- Observation attendue : Vous ne verrez que l'erreur générée par l'exception. Le message affiché par echo ne s'affichera pas, car une exception non interceptée stoppe immédiatement l'exécution du script.
- Initialiser le projet :
Exo - Gestion des incidents 01-02 : Intercepter une exception avec try / catch
- Objectif : Intercepter une exception pour éviter qu'elle n'interrompe le script, et gérer proprement les erreurs côté développeur.
- Instructions :
- Préparer le fichier :
- Dupliquer le fichier exo-01-01.php.
- Renommer cette copie en exo-01-02.php.
- Encadrer l'ensemble du code de l'exercice précédent dans un bloc try.
- Remplacer le texte de l'echo présent dans ce bloc try par : "Le code situé après la levée d'exception dans un même bloc try..."
- Créer un bloc catch juste après le bloc try, destiné à intercepter l'exception levée.
- Dans le bloc catch, afficher le message de l'exception à l'aide de la méthode getMessage().
- Ajouter à la suite du bloc catch une instruction echo affichant le texte : "Le code situé après les blocs try et catch..."
- Observation attendue : Cette fois :
- Le message du bloc catch s'affiche correctement à la place du message d'erreur système.
- Le message "Le code situé après les blocs try et catch..." s'affiche également, prouvant que le script ne s'est pas interrompu.
- Le message situé juste après throw à l'intérieur du bloc try ne s'affichera pas, car la levée d'exception interrompt l'exécution à cet endroit précis.
- Préparer le fichier :
Exo - Gestion des incidents 01-03 : Remontée d'une exception à travers plusieurs fonctions
- Objectif : Comprendre comment une exception se propage d'une fonction à une autre jusqu'à être interceptée par un bloc try approprié.
- Instructions :
- Créer un nouveau fichier nommé exo-01-03.php dans le dossier exo-gestion-des-incidents-01.
- Dans ce fichier, créer une fonction nommée importerEtConvertirListeJson() qui :
- Prend en paramètre un chemin de fichier (sous forme de chaîne de caractères).
- Vérifie si le fichier existe avec file_exists().
- Si le fichier n'existe pas, lève une exception avec un message explicite.
- Sinon, retourner le contenu du fichier JSON en tableau.
- Créer une seconde fonction nommée afficherListeUtilisateur() qui :
- Construit le chemin vers un fichier listeUtilisateur.json placé dans un sous-dossier stockage.
- Appelle la fonction importerEtConvertirListeJson().
- Affiche la liste retournée.
- Dans le code principal (en dehors des fonctions), créer un bloc try qui :
- Appelle la fonction afficherListeUtilisateur().
- Intercepte l'exception dans un bloc catch associé, et affiche le message d'erreur avec getMessage().
- Ajouter à la fin du script une instruction echo affichant : "Le code situé après les blocs try et catch...".
- Exécuter le script dans le navigateur et observer son comportement :
- Le fichier listeUtilisateur.json n'existant pas, une exception est levée dans importerEtConvertirListeJson().
- Cette exception n'étant pas interceptée dans cette fonction, elle remonte dans la fonction appelante, puis encore jusqu'au bloc try du code principal, où elle est enfin interceptée.
- Le message défini dans l'exception s'affiche correctement.
- Le message "Le code situé après les blocs try et catch..." s'affiche également, prouvant que le script a pu continuer malgré l'erreur.
Les différentes catégories d'incidents
Depuis PHP 7, une partie importante des incidents auparavant considérés comme des erreurs fatales classiques est désormais représentée par des objets de la hiérarchie Error. Ces objets implémentent, comme les exceptions classiques, l'interface Throwable.
Concrètement, cela signifie qu'un bloc catch peut intercepter aussi bien une Exception levée par le développeur qu'un objet de type Error levé par PHP ou par certaines fonctions internes.
En revanche, il faut éviter une confusion fréquente. Les warnings, notices et deprecated ne deviennent pas automatiquement des objets Throwable. Ils ne sont donc pas capturés naturellement par un catch.
Voici une vue simplifiée de la hiérarchie :
Throwable
├── Error
│ ├── ArithmeticError
│ │ └── DivisionByZeroError
│ ├── TypeError
│ │ └── ArgumentCountError
│ ├── ValueError
│ ├── ParseError
│ └── AssertionError
└── Exception
├── LogicException
│ ├── InvalidArgumentException
│ └── OutOfRangeException
├── RuntimeException
│ ├── OverflowException
│ └── UnderflowException
├── PDOException
├── ErrorException
├── Exception personnalisée...
└── ...
Techniquement, il est possible de lever manuellement un objet de type Error avec throw new Error(...). Cependant, ce n'est généralement pas une bonne pratique dans un code applicatif. En pratique, lorsque le développeur veut signaler un incident métier ou technique, il privilégiera une Exception ou une exception personnalisée.
<?php
try
{
// Ceci fonctionne techniquement,
// mais ce n'est généralement pas l'outil à privilégier dans un code applicatif.
throw new Error('Erreur technique levée manuellement');
}
catch (Error $e)
{
echo 'Erreur interceptée : ' . $e->getMessage();
}
?>
Pour signaler explicitement un incident dans son propre code, il vaut donc mieux utiliser une exception classique ou personnalisée.
Attraper spécifiquement les erreurs et les exceptions
Un bloc catch n'intercepte que les objets du type demandé, ou les objets d'une classe enfant compatible. C'est un point capital. Un catch (Exception $e) n'attrape pas un objet de type Error.
Pour un exemple stable et clair, prenons la fonction intdiv() qui retourne toujours le résultat entier d'une division. Si son second argument vaut 0, PHP lève une DivisionByZeroError, qui fait partie de la hiérarchie Error.
<?php
try
{
// intdiv() avec un diviseur à 0 provoque une DivisionByZeroError.
echo intdiv(10, 0);
}
catch (Exception $e)
{
// Ce bloc ne sera pas exécuté,
// car DivisionByZeroError hérite de la classe Error et non de la classe Exception.
echo 'Une exception a été levée : ' . $e->getMessage();
}
?>
Gérer différents types d'incidents provenant d'un même bloc try
Si l'on veut réagir différemment selon le type d'incident, on peut enchaîner plusieurs blocs catch. Le plus spécifique doit être placé avant le plus général.
<?php
try
{
echo intdiv(10, 0);
}
catch (DivisionByZeroError $e)
{
echo 'Erreur de division par zéro : ' . $e->getMessage();
}
catch (Error $e)
{
echo 'Une erreur PHP est survenue : ' . $e->getMessage();
}
catch (Exception $e)
{
echo 'Une exception a été levée : ' . $e->getMessage();
}
?>
Ici, PHP commence par tester le premier bloc catch. Comme l'incident est précisément une DivisionByZeroError, c'est ce bloc qui est exécuté.
Gérer différents types d'incidents dans un même bloc catch
Il est aussi possible d'utiliser catch (Throwable $t) si l'on veut intercepter à la fois les objets de type Exception et ceux de type Error.
<?php
try
{
echo intdiv(10, 0);
}
catch (Throwable $t)
{
if ($t instanceof Error)
{
echo 'Une erreur PHP est survenue : ' . $t->getMessage();
}
elseif ($t instanceof Exception)
{
echo 'Une exception a été détectée : ' . $t->getMessage();
}
}
?>
Cette forme peut être pratique pour un gestionnaire générique, mais elle est parfois moins lisible qu'une série de blocs catch bien ciblés. Lorsqu'on connaît les types que l'on souhaite distinguer, plusieurs blocs dédiés restent souvent plus clairs pour un débutant.
Déboguer les incidents
Lorsqu'un incident est capturé, l'objet reçu dans le bloc catch contient plusieurs informations utiles. Le message ne suffit pas toujours. Pour diagnostiquer correctement un problème, il est souvent utile de connaître aussi le type, le code, le fichier, la ligne et la pile d'appels.
Les méthodes suivantes sont particulièrement utiles :
- getMessage() pour obtenir le message associé à l'incident.
- getCode() pour obtenir un code numérique éventuel.
- getFile() et getLine() pour localiser l'origine de l'incident.
- getTrace() et getTraceAsString() pour obtenir la pile d'exécution.
- __toString() pour obtenir une représentation textuelle plus complète de l'objet.
Pour bien visualiser ces informations, nous allons provoquer une exception dans une fonction, puis l'intercepter plus haut. Cela permettra de voir apparaître plusieurs niveaux d'appels dans la trace.
Fichier fonction.php
<?php
// Fichier : fonction.php
function genererException(): void
{
// Cette fonction simule un incident en levant une exception.
// Le second argument est un code numérique libre.
// Il peut servir à classer plus facilement certains incidents.
throw new Exception('Une erreur s\'est produite lors du traitement.', 500);
}
function operation(): void
{
// Cette fonction sert d'intermédiaire.
// Grâce à elle, la trace d'exécution contiendra plusieurs niveaux d'appels.
genererException();
}
?>
Fichier index.php
<?php
// Fichier : index.php
require_once 'fonction.php';
try
{
operation();
}
catch (Throwable $e)
{
// Construire une structure de log lisible.
$log = [
'date' => date('Y-m-d H:i:s'),
'type' => get_class($e),
'code' => $e->getCode(),
'message' => $e->getMessage(),
'fichier' => $e->getFile(),
'ligne' => $e->getLine(),
'trace' => $e->getTrace()
];
// Encoder le tableau sous forme de texte JSON lisible.
// JSON_UNESCAPED_SLASHES permet d'échapper les slashs (éviter c:\/).
// JSON_UNESCAPED_UNICODE permet de garder les accents lisibles (éviter \u00e9chec).
// JSON_PRETTY_PRINT permet d'indenter le JSON pour le rendre plus lisible.
$logJson = json_encode(
$log,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
);
echo '<h2 style="color: red;">Incident détecté</h2>';
echo "<pre>{$logJson}</pre>";
}
?>
Exemple de résultat affiché
{
"date": "2026-04-10 10:00:00",
"type": "Exception",
"code": 500,
"message": "Une erreur s'est produite lors du traitement.",
"fichier": "/var/www/html/fonction.php",
"ligne": 9,
"trace": [
{
"file": "/var/www/html/fonction.php",
"line": 16,
"function": "genererException"
},
{
"file": "/var/www/html/index.php",
"line": 8,
"function": "operation"
}
]
}
Ce type d'affichage est très utile en développement, mais il faut éviter de le montrer tel quel aux utilisateurs d'un site en production. Ces informations sont précieuses pour le développeur, mais trop détaillées pour un visiteur ordinaire.
Journaliser un incident avec error_log()
Pendant le développement, afficher les détails d'un incident directement dans le navigateur peut faire gagner du temps. On voit immédiatement le type de problème, le message, le fichier, la ligne et parfois la trace complète.
En production, ce comportement devient risqué. Afficher des détails techniques à l'utilisateur peut révéler la structure du projet, le nom de certains fichiers, des chemins internes, voire des éléments utiles à un attaquant. Il vaut mieux enregistrer ces informations dans un fichier de log et afficher un message générique côté interface.
Pour enregistrer ce type d'information, PHP fournit notamment la fonction error_log().
Cette fonction permet d'envoyer un message vers différents endroits. Selon la valeur du deuxième paramètre, le message peut être envoyé vers le système de log configuré pour PHP, vers un courriel, vers un fichier choisi manuellement, ou encore vers le gestionnaire de logs utilisé par l'environnement dans lequel PHP s'exécute.
Dans un petit projet pédagogique, le cas le plus simple à comprendre est souvent le type 3, car il permet d'écrire directement dans un fichier précis, par exemple erreurs.log.
error_log(string $message, int $type = 0, string $destination = null, string $extra_headers = null);
Les paramètres
- string $message correspond au message à enregistrer.
- int $type indique où ce message doit être envoyé.
- ?string $destination précise la destination lorsqu'elle est nécessaire.
- ?string $extra_headers sert uniquement dans le cas d'un envoi par courriel.
Les principaux types disponibles sont les suivants
- 0 envoie le message vers le système de journalisation utilisé par PHP. En pratique, cela dépend de la configuration du serveur et de la directive error_log définie dans php.ini. C'est la valeur utilisée par défaut.
- 1 envoie le message par courriel à l'adresse indiquée dans $destination. Cette option peut être utile pour recevoir une alerte lorsqu'un incident grave survient. Elle dépend toutefois de la configuration de l'envoi d'e-mails sur le serveur, car elle utilise le même mécanisme interne que la fonction mail().
- 2 n'est plus une option.
- 3 ajoute le message dans un fichier que l'on choisit soi-même. C'est souvent l'option la plus facile à comprendre et à tester dans un exercice.
- 4 envoie le message vers le système de logs utilisé par l'environnement dans lequel PHP tourne. Autrement dit, PHP confie le message à l'outil qui gère déjà les journaux du serveur ou de l'exécution en cours. Cette option est plus technique et, dans un premier apprentissage, le type 3 reste généralement plus simple à comprendre.
Pour débuter, retenez surtout ceci. Le type 0 laisse PHP choisir la destination selon sa configuration, tandis que le type 3 permet d'écrire directement dans un fichier précis. Ce sont les deux cas les plus utiles à distinguer à ce stade.
Le type 3 demande une petite attention. La documentation de PHP précise que, dans ce mode, aucun saut de ligne n'est ajouté automatiquement à la fin du message. Si plusieurs messages sont écrits à la suite, ils risquent donc d'être collés les uns aux autres dans le fichier. Dans ce cas, il faut ajouter soi-même un retour à la ligne, par exemple avec PHP_EOL.
<?php
$message = 'Incident de test' . PHP_EOL;
// Enregistrer le message dans un fichier précis.
error_log($message, 3, __DIR__ . '/erreurs.log');
?>
Dans un projet réel, on peut aller plus loin. Par exemple, enregistrer certains incidents dans un fichier, en envoyer d'autres vers le système de logs du serveur, ou déclencher une alerte uniquement pour les problèmes les plus graves. Mais dans un premier temps, l'objectif principal est surtout de comprendre où le message part et pourquoi le type 3 est souvent le plus simple à manipuler dans un projet pédagogique.
Centraliser la gestion des incidents
Pour éviter de répéter la même logique partout, il est utile de centraliser cette gestion dans quelques fonctions dédiées. Nous allons distinguer deux rôles.
- gererLog() se charge du traitement final du log selon le mode actif.
- gererIncidents() prépare une structure commune à partir d'un objet Throwable.
Fichier gererIncidents.php
<?php
// Fichier : gererIncidents.php
function gererLog(array $log, string $fichierLog, bool $afficherMessageUtilisateur = false): void
{
// Transformer le tableau en texte JSON lisible.
$logMessage = json_encode(
$log,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
);
// Sécurité minimale : si l'encodage échoue,
// on prépare malgré tout un message de secours.
if ($logMessage === false)
{
$logMessage = '{"message":"Impossible d\'encoder le log au format JSON."}';
}
// Le type 3 de error_log() n'ajoute pas automatiquement de saut de ligne.
// Pour rendre le fichier de log plus lisible, on ajoute donc une séparation
// entre chaque entrée à l'aide d'un retour à la ligne, d'une ligne de 80 tirets,
// puis d'un nouveau retour à la ligne.
$logMessage .= PHP_EOL . str_repeat('-', 80) . PHP_EOL;
if (defined('DEV_MODE') && DEV_MODE === true)
{
echo '<h2 style="color: red;">Incident détecté</h2>';
echo "<pre>{$logMessage}</pre>";
if ($afficherMessageUtilisateur)
{
echo '<p>Un message générique pourrait être affiché à l\'utilisateur dans un vrai projet.</p>';
}
}
else
{
$cheminLog = __DIR__ . DIRECTORY_SEPARATOR . $fichierLog;
error_log($logMessage, 3, $cheminLog);
if ($afficherMessageUtilisateur)
{
echo 'Un incident inattendu s\'est produit. Veuillez réessayer plus tard.';
}
}
}
function gererIncidents(Throwable $e, bool $afficherMessageUtilisateur = false): void
{
$log = [
'date' => date('Y-m-d H:i:s'),
'type' => get_class($e),
'code' => $e->getCode(),
'message' => $e->getMessage(),
'fichier' => $e->getFile(),
'ligne' => $e->getLine(),
'trace' => $e->getTrace()
];
gererLog($log, 'erreurs.log', $afficherMessageUtilisateur);
}
?>
Cette séparation apporte une meilleure souplesse. Si l'on souhaite plus tard créer un format particulier pour certaines exceptions, il suffira d'ajouter une fonction de préparation spécialisée, tout en conservant la même fonction de sortie finale pour l'affichage ou la journalisation.
Exemple d'intégration dans index.php
<?php
// Fichier : index.php
define('DEV_MODE', true);
require_once __DIR__ . DIRECTORY_SEPARATOR . 'gererIncidents.php';
try
{
// Bloc de code à exécuter.
}
catch (Throwable $e)
{
gererIncidents($e);
}
?>
Exercices - Partie 02
Exo - Gestion des incidents 02-01 : Gestion des incidents en mode développement et production
- Objectif :
- Comprendre et tester la gestion des incidents dans une application web PHP simulant un terminal de paiement (Bancontact).
- Observer le comportement de l'application en cas d'incidents utilisateur et simuler un mécanisme de blocage de carte après plusieurs tentatives.
- Instructions :
- Initialiser le projet :
- Téléchargez le projet et décompressez-le dans votre environnement de travail.
- Examinez la structure du projet pour comprendre son organisation (modèle MVC simplifié).
- Tester l'application :
- Lancez l'application dans votre navigateur. Il s'agit d'une simulation de Bancontact permettant de saisir un code secret.
- Dans un premier temps, entrez des codes aléatoires (évitez 1234). Observez le nombre de tentatives restantes et les messages affichés.
- Rafraîchissez la page (tapez Entrée dans la barre d'adresse) pour réinitialiser la session.
- Entrez le code correct (1234). Vérifiez que le message de validation s'affiche et que la carte est restituée.
- Après trois tentatives incorrectes, vérifiez que la carte est "avalée".
- Explorer chacun des fichiers suivants et s'assurer d'en comprendre le contenu :
- index.php
- Ce fichier est ce qu'on appelle une "vue". Il s'occupe uniquement de l'affichage de l'interface utilisateur (formulaire HTML, messages d'erreur ou de validation).
- Il contient très peu de logique. Son rôle est d'afficher les informations préparées par le contrôleur (état de la carte, nombre de tentatives, messages, etc.).
- bancontactController.php
- Ce fichier est le "contrôleur" de l'application. Il joue le rôle de chef d'orchestre et décide quoi faire selon les actions de l'utilisateur.
- Il analyse les données envoyées par le formulaire, valide le code saisi, gère le nombre de tentatives et prépare les messages à afficher dans la vue.
- Il fait le lien entre ce que l'utilisateur envoie (les données du formulaire) et ce que l'application doit afficher ou exécuter.
- bancontactModel.php
- Ce fichier joue le rôle de "modèle". Il contient les fonctions qui traitent les données de l'application, comme la vérification du code saisi ou la simulation du blocage d'une carte.
- Dans une vraie application, c'est ici que l'on irait interroger une base de données pour comparer un code ou enregistrer une action.
- Ici, les données sont simulées dans le code, mais le fonctionnement général reste le même. Ce fichier représente la partie "logique métier".
- gestionFormulaire.php
- Ce fichier contient une série de fonctions utilitaires dédiées à la validation des formulaires HTML.
- Il vérifie par exemple si un champ est rempli, s'il est numérique, ou s'il respecte une longueur minimale ou maximale.
- Ces fonctions sont indépendantes du projet et peuvent être réutilisées ailleurs. On peut voir ce fichier comme une "boîte à outils" pour fiabiliser les données envoyées par les utilisateurs.
- index.php
- Développer le gestionnaire des incidents :
- Dans le fichier index.php, définissez une constante MODE_DEV en lui attribuant la valeur true (au tout début du fichier).
- Dans le fichier gererIncidents.php, créez une fonction nommée gererIncidents() permettant de gérer les incidents de manière centralisée.
- La fonction devra :
- Afficher un message d'erreur détaillé dans le navigateur si l'application est en mode développement (si MODE_DEV vaut true).
- Écrire un message d'erreur formaté dans un fichier du dossier logs si l'application est en mode production (si MODE_DEV vaut false).
- Actualiser le fichier "bancontactController.php" pour qu'il appelle le gestionnaire d'erreur lorsqu'une carte est bloquée :
- Importez le fichier gererIncidents.php en haut du contrôleur, aux côtés des autres dépendances.
- Ajoutez un bloc try/catch autour de la partie du code qui peut déclencher une exception dans la fonction traiterBancontact().
- Modifier le code pour qu'il lève une exception lorsqu'une carte est bloquée.
- En cas d'erreur, appelez la fonction de gestion d'erreur que vous avez définie dans gererIncidents.php et passez lui le message d'erreur.
- Même si une exception est levée, l'utilisateur doit continuer de recevoir le message lui indiquant que sa carte a été bloquée.
- Tester en mode développement
- Vérifiez que la constante MODE_DEV est bien définie sur true dans le fichier index.php.
- Entrez trois codes incorrects afin de vous assurer que, en mode développement, les messages d'erreur destinés au développeur s'affichent immédiatement dans le navigateur.
- Tester en mode production
- Modifiez la constante MODE_DEV et définissez-la sur false.
- Entrez à nouveau trois codes incorrects pour vérifier qu'en mode production, les messages d'erreur destinés aux administrateurs sont bien enregistrés dans le fichier de log configuré.
- Initialiser le projet :
Le bloc finally
Le bloc finally sert à exécuter un morceau de code après un try et, s'il existe, après un catch. Il est particulièrement utile pour les opérations de nettoyage, comme fermer un fichier, libérer une ressource ou rétablir un état temporaire.
Il faut toutefois employer une formulation précise. Dans le déroulement normal des exceptions, finally s'exécute qu'il y ait eu un incident ou non. En revanche, si le script est interrompu par exit(), le bloc finally n'est pas exécuté.
<?php
try
{
throw new Exception('Erreur simulée');
}
catch (Throwable $e)
{
echo 'Incident capturé : ' . $e->getMessage();
}
finally
{
echo 'Ceci s\'exécute après le try/catch dans le déroulement normal.';
}
?>
Ici, l'exception est capturée, puis le bloc finally est exécuté.
Voici un cas où aucun incident ne survient :
<?php
try
{
echo 'Tout fonctionne correctement.';
}
catch (Throwable $e)
{
echo 'Incident capturé : ' . $e->getMessage();
}
finally
{
echo 'Ceci s\'exécute aussi lorsqu\'aucun incident n\'est survenu.';
}
?>
Un cas d'usage très fréquent concerne les fichiers. Si un fichier a été ouvert, il vaut mieux prévoir sa fermeture dans un bloc finally.
<?php
try
{
// Tenter d'ouvrir un fichier en écriture.
$fichier = fopen('exemple.txt', 'w');
// fopen() ne lève pas d'exception par défaut.
// En cas d'échec, la fonction retourne false.
// Il faut donc vérifier le résultat.
if ($fichier === false)
{
throw new Exception('Impossible d\'ouvrir le fichier.');
}
$texte = 'Bonjour, ceci est un test.';
$octetsEcrits = fwrite($fichier, $texte);
// fwrite() retourne false en cas d'échec complet.
// Une valeur différente de strlen($texte) indique une écriture partielle.
if ($octetsEcrits === false || $octetsEcrits !== strlen($texte))
{
throw new Exception('Échec ou écriture partielle du fichier.');
}
echo 'Écriture réussie.';
}
catch (Throwable $e)
{
echo 'Incident : ' . $e->getMessage();
}
finally
{
// Fermer le fichier seulement s'il a bien été ouvert.
if (isset($fichier) && is_resource($fichier))
{
fclose($fichier);
echo ' Fichier fermé.';
}
}
?>
Dans cet exemple, le fichier est refermé si nécessaire, même si une exception a été levée pendant le traitement. C'est précisément le genre de tâche pour lequel finally est très utile.
Incidents personnalisés
Jusqu'ici, nous avons utilisé des objets fournis directement par PHP, comme Exception ou certains objets de la hiérarchie Error. Mais il est aussi possible de créer ses propres types d'exceptions.
L'intérêt principal est de rendre le code plus explicite. Une exception personnalisée permet de dire plus clairement de quel type de problème il s'agit. Cela rend aussi les blocs catch plus lisibles, car ils peuvent cibler un contexte précis.
Par exemple, si un script communique avec un service bancaire externe, il peut être utile de distinguer les incidents liés à cette API des autres incidents de l'application.
<?php
class ApiBanqueException extends Exception
{
// Aucun code supplémentaire n'est obligatoire.
// Le simple fait de créer une classe dédiée permet déjà
// d'identifier plus clairement ce type d'incident.
}
?>
On peut ensuite l'utiliser comme n'importe quelle autre exception :
<?php
if (...)
{
throw new ApiBanqueException('Authentification échouée : clé API rejetée.');
}
?>
L'avantage apparaît surtout au moment de la capture. Un bloc catch dédié à ApiBanqueException permet d'appliquer un traitement particulier, par exemple écrire dans un fichier de log spécifique, enregistrer le contexte de la requête, puis afficher un message plus neutre à l'utilisateur.
<?php
$requete = [
'endpoint' => 'https://api.banque.exemple.com/solde',
'methode' => 'GET',
'params' => ['compte' => 'BE1234567890']
];
try
{
$solde = appelApi($requete['endpoint'], $requete['methode'], $requete['params']);
echo "Solde disponible : {$solde} €";
}
catch (ApiBanqueException $e)
{
// Gestion ciblée pour les incidents liés à l'API bancaire.
gererApiBanqueIncidents($e, $requete);
// Message neutre destiné à l'utilisateur final.
echo 'Service temporairement inaccessible, veuillez réessayer plus tard.';
}
catch (Throwable $e)
{
// Gestion générique pour les autres incidents.
gererIncidents($e);
}
?>
On peut alors préparer un log spécialisé, plus riche que le log générique, parce qu'il contient des informations utiles au contexte de l'appel externe.
<?php
function gererApiBanqueIncidents(Throwable $e, array $requete = []): void
{
$log = [
'timestamp' => date('Y-m-d H:i:s'),
'type' => get_class($e),
'message' => $e->getMessage(),
'requete' => [
'endpoint' => $requete['endpoint'] ?? 'non précisé',
'methode' => $requete['methode'] ?? 'non précisé',
'params' => $requete['params'] ?? []
],
'emplacement' => [
'fichier' => $e->getFile(),
'ligne' => $e->getLine()
],
'trace' => $e->getTrace()
];
gererLog($log, 'apiBanque.log');
}
?>
Dans un code applicatif, il vaut mieux réserver les objets Error aux incidents levés naturellement par PHP. Pour ses propres besoins, le développeur privilégiera presque toujours des exceptions personnalisées.
Gestion automatique des incidents
Jusqu'ici, les incidents étaient capturés manuellement grâce à des blocs try/catch. Cette approche reste indispensable lorsqu'on veut réagir localement, au bon endroit. Mais elle ne suffit pas toujours.
Que faire lorsqu'un incident se produit en dehors d'un bloc try/catch prévu à l'avance ? Que faire pour les warnings et autres messages du moteur PHP qui ne passent pas naturellement par ce mécanisme ? Que faire enfin lorsqu'un script s'arrête brutalement à cause d'une erreur fatale ?
PHP propose plusieurs mécanismes complémentaires :
- set_exception_handler() pour les objets Throwable non interceptés.
- set_error_handler() pour de nombreux warnings, notices, deprecated et erreurs utilisateur.
- register_shutdown_function() pour exécuter une fonction en fin de script et inspecter la dernière erreur éventuelle.
Ces mécanismes sont complémentaires. Aucun d'eux ne remplace complètement les autres. Il faut donc bien comprendre leur rôle respectif.
set_exception_handler()
Cette fonction permet de définir un gestionnaire global pour les objets Throwable qui n'ont pas été interceptés par un bloc try/catch.
<?php
set_exception_handler('gererIncidents');
?>
Cela signifie que si une exception ou un objet de type Error remonte jusqu'au niveau global sans avoir été capturé, la fonction indiquée sera appelée.
Il faut toutefois retenir une idée importante. Ce gestionnaire global agit comme un filet de sécurité final. Il ne remplace pas les blocs try/catch bien placés dans le code. Une fois ce gestionnaire appelé, l'exécution normale du script ne reprend pas.
<?php
set_exception_handler('gererAutoIncidents');
function gererAutoIncidents(Throwable $e): void
{
// Le second argument indique qu'un message générique
// peut être affiché à l'utilisateur en production.
gererIncidents($e, true);
}
?>
En résumé, utiliser set_exception_handler() ne dispense pas de réfléchir à l'endroit le plus pertinent pour capter localement un incident. Il sert surtout à éviter qu'un incident non prévu ne passe totalement inaperçu.
set_error_handler()
Les warnings, notices, deprecated et erreurs utilisateur de type E_USER_WARNING, E_USER_NOTICE ou E_USER_DEPRECATED ne sont pas capturés naturellement par les blocs try/catch. Pour les traiter, on peut définir un gestionnaire personnalisé avec set_error_handler().
<?php
set_error_handler('gererErreursWarnings');
?>
La fonction associée reçoit plusieurs informations, notamment le niveau de l'erreur, son message, le fichier et la ligne.
<?php
function gererErreursWarnings(int $niveau, string $message, string $fichier, int $ligne): void
{
$log = [
'timestamp' => date('Y-m-d H:i:s'),
'type' => 'erreurWarning',
'niveau' => $niveau,
'message' => $message,
'fichier' => $fichier,
'ligne' => $ligne
];
gererLog($log, 'phpErreurWarnings.log');
}
?>
Il faut toutefois connaître une limite importante. set_error_handler() ne peut pas prendre en charge les niveaux E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR et E_COMPILE_WARNING.
Il faut aussi savoir qu'un warning ne devient pas automatiquement une exception. Si l'on souhaite faire entrer certains warnings dans une logique try/catch, il faut les convertir explicitement, par exemple en lançant une ErrorException depuis le gestionnaire d'erreurs.
<?php
set_error_handler(function (int $niveau, string $message, string $fichier, int $ligne): void
{
throw new ErrorException($message, 0, $niveau, $fichier, $ligne);
});
?>
Cette technique est utile dans certains projets, mais pour des débutants, il vaut mieux d'abord bien distinguer les warnings du moteur et les exceptions applicatives avant de chercher à tout unifier.
register_shutdown_function()
Cette fonction permet d'enregistrer un rappel qui sera exécuté à la fin du script, y compris si l'exécution se termine par exit().
<?php
register_shutdown_function('gererErreursFatales');
?>
Ici encore, il faut employer les bons mots. Ce mécanisme n'attrape pas une erreur fatale comme un bloc catch attrape une exception. Il permet surtout d'exécuter une fonction en fin de script, puis d'inspecter la dernière erreur connue avec error_get_last().
<?php
function gererErreursFatales(): void
{
$erreur = error_get_last();
if (
$erreur
&& in_array(
$erreur['type'],
[E_ERROR, E_PARSE, E_COMPILE_ERROR, E_CORE_ERROR],
true
)
)
{
$log = [
'timestamp' => date('Y-m-d H:i:s'),
'type' => 'erreurFatale',
'niveau' => $erreur['type'],
'message' => $erreur['message'],
'fichier' => $erreur['file'],
'ligne' => $erreur['line']
];
gererLog($log, 'phpErreursFatales.log', true);
}
}
?>
Cette approche est utile pour éviter qu'un incident fatal se traduise uniquement par une page blanche ou par une sortie difficile à exploiter. Elle permet de journaliser les dernières informations connues, mais elle ne remet pas le script en route.
Il faut également connaître une limite supplémentaire. Certaines erreurs peuvent survenir avant même que le script n'ait eu le temps d'enregistrer ses gestionnaires personnalisés. C'est notamment le cas de certaines erreurs de syntaxe ou de compilation.
Autrement dit, même un dispositif global bien préparé ne permet pas de tout rattraper. D'où l'intérêt de cumuler plusieurs niveaux de protection. Du code soigné, des validations simples avec des conditions, des exceptions bien placées, puis un filet global pour les cas non anticipés.
Exercices - Partie 03
Exo - Gestion des incidents 02-02 : Gestion centralisée avec erreurs et exceptions personnalisées
- Objectif :
- Approfondir la gestion des incidents en PHP avec des exceptions personnalisées et une interception automatique des erreurs.
- Tester l'enregistrement des incidents dans différents fichiers selon leur nature.
- Instructions :
- Préparer le projet :
- Dupliquez le dossier du projet de l'exercice php-exo-gestion-des-incidents-02-01 et renommez-le en php-exo-gestion-des-incidents-02-02.
- Gardez la structure du projet identique (vue, contrôleur, modèle, etc.).
- Ajouter la gestion automatique des incidents :
- Dans le fichier gestionIncidents.php, créer une fonction initialiserInterceptionAutomatiqueDesErreurs() permettant de gérer :
- Les exceptions non capturées (set_exception_handler()) devront appeler la fonction déjà définie lors de l'exercice suivant gererIncidents() dont l'objectif était de gérer les exceptions génériques.
- Les erreurs non bloquantes du moteur PHP (set_error_handler()) devront appeler la fonction gererErreursWarnings() qui reste à définir.
- Les erreurs fatales du moteur PHP (register_shutdown_function()) devront appeler la fonction gererErreursFatales() qui reste à définir..
- Dans le fichier gestionIncidents.php, créer une fonction initialiserInterceptionAutomatiqueDesErreurs() permettant de gérer :
- Toujours dans le fichier gestionIncidents.php, créer les fonctions appelées par set_error_handler() et register_shutdown_function() :
- gererErreursWarnings() : Pour gérer les erreurs non bloquantes. Elle devra recevoir les 4 paramètres prévus par PHP (int $niveau, string $message, string $fichier, int $ligne) et appeler la fonction gererLog() pour enregistrer les informations dans un fichier dédié (phpErreursWarnings.log).
- gererErreursFatales() : Pour intercepter les erreurs fatales (error_get_last()) et appeler la fonction gererLog() pour enregistrer les informations dans un fichier dédié (phpErreursFatales.log).
- Créer une exception personnalisée :
- Créer un dossier Exceptions à la racine du proejt
- Dans ce dossier, créer un nouveau fichier PHP contenant une classe nommée ApiBanqueException qui hérite de la classe Exception.
- Cette exception servira à signaler qu'une carte a été bloquée après plusieurs tentatives.
- Modifier le contrôleur bancontactController.php :
- Importez la nouvelle exception ApiBanqueException.
- Modifiez la fonction gererTentatives() pour lever cette exception lorsque le nombre de tentatives autorisées est dépassé.
- Dans traiterBancontact(), ajoutez un bloc catch destiné à ce type d'exception (ApiBanqueException).
- Dans ce bloc, formatez le log avec les informations suivantes avant de les envoyer à la fonction gererLog() pour l'enregistrer dans le fichier apiBanque.php :
- timestamp avec la date actuelle (date('Y-m-d H:i:s')).
- message avec le message configuré lors de la levée d'exception.
- numCarte avec le numéro de la carte.
- compte avec le numéro de compte.
- utilisateur avec le nom et le prénom de l'utilisateur.
- Tester votre application dans les deux modes (développement et production) :
- Testez une saisie correcte du code PIN (1234) : la carte doit être restituée.
- Testez plusieurs codes erronés : la carte doit finir par être bloquée, et le log correspondant doit être généré.
- Faites un test avec une erreur volontaire (comme un appel de fonction inexistante) pour vérifier que gererErreursFatales() fonctionne.
- Préparer le projet :