Accueil >PHP >PHP orienté objet >Exceptions et classes d’exception personnalisées

Exceptions et classes d’exception personnalisées

Une exception interrompt le code et transmet l’erreur à l’endroit qui sait la traiter. Comment lever et intercepter des exceptions, pourquoi catch(Exception) n’intercepte pas tout, comment écrire votre propre exception et ce qui arrive à une exception que personne n’a interceptée.

throw, try, catch et finally

Une fonction qui ne peut pas terminer son travail lève une exception (throw). Le code est interrompu et continue dans le bloc catch le plus proche qui intercepte l’exception. Le bloc finally s’exécute toujours – après un succès, après une exception interceptée et aussi lors d’une exception qui continue sa route :

function process()
  {
    try
      {
        echo "traitement\n";
        throw new RuntimeException("Le fichier n'a pas pu être lu");
      }
    finally
      {
        echo "nettoyage\n";
      }
  }
try
  {
    process();
    echo "ceci ne s'affiche pas\n";
  }
catch(RuntimeException $e)
  {
    echo "intercepté : ".$e->getMessage();
  }
traitement
nettoyage
intercepté : Le fichier n'a pas pu être lu

finally est l’endroit du nettoyage qui doit toujours s’exécuter : fermer un fichier, libérer un verrou, rétablir un réglage.

Error et Exception

Toutes les exceptions implémentent l’interface Throwable et se divisent en deux branches :

Throwable
├── Error – erreurs dans le code
│   ├── TypeError
│   │   └── ArgumentCountError
│   ├── ValueError (depuis PHP 8.0)
│   ├── ArithmeticError
│   │   └── DivisionByZeroError
│   └── UnhandledMatchError (depuis PHP 8.0)
└── Exception – erreurs à l’exécution que le programme prévoit
    ├── RuntimeException
    │   └── PDOException
    ├── LogicException
    │   └── InvalidArgumentException
    └── JsonException

C’est pourquoi catch(Exception $e) n’intercepte pas une erreur de type Error, par exemple une division entière par zéro :

try
  {
    try
      {
        echo intdiv(10, 0);
      }
    catch(Exception $e)
      {
        echo "Exception\n";
      }
  }
catch(Throwable $t)
  {
    echo get_class($t).": ".$t->getMessage();
  }
DivisionByZeroError: Division by zero

Le bloc intérieur n’intercepte pas l’erreur, seul catch(Throwable $t) le fait. Au niveau le plus haut (dans le fichier d’entrée du site, dans le traitement des exceptions non interceptées), interceptez Throwable.

Un seul bloc intercepte plusieurs types d’exceptions si vous l’écrivez catch(PDOException | JsonException $e) (depuis PHP 7.1). Les blocs catch sont vérifiés à partir du haut, le type plus général se place donc sous le plus précis.

Exceptions personnalisées

Une classe d’exception à vous permet d’intercepter exactement un type d’erreur et de transmettre des données supplémentaires :

class NotFoundException extends RuntimeException
  {
    private string $what;
    public function __construct(string $what, int $id)
      {
        parent::__construct($what." avec l'id ".$id." n'existe pas");
        $this->what = $what;
      }
    public function what(): string
      {
        return $this->what;
      }
  }
function findArticle(int $id): array
  {
    $articles = [1 => ["title" => "Classes et objets"]];
    if(!isset($articles[$id])) throw new NotFoundException("Article", $id);
    return $articles[$id];
  }
try
  {
    echo findArticle(1)["title"]."\n";
    echo findArticle(7)["title"]."\n";
  }
catch(NotFoundException $e)
  {
    echo $e->getMessage()." (".$e->what().")";
  }
Classes et objets
Article avec l'id 7 n'existe pas (Article)

Sur un site, le bloc catch contiendrait la page 404 – le code de réponse par http_response_code(404) doit être défini avant que quoi que ce soit ne soit affiché.

Les exceptions personnalisées étendent la classe intégrée adéquate – généralement RuntimeException (erreur pendant l’exécution) ou InvalidArgumentException (mauvaises données d’entrée d’une fonction). Elles n’étendent pas Error, cette branche appartient aux erreurs de PHP.

Transmettre la cause

Quand vous interceptez une exception et levez la vôtre, transmettez l’exception d’origine comme troisième argument. La cause ne se perd pas et getPrevious() la renvoie :

class ImportException extends RuntimeException
  {
  }
function loadSettings(string $json): array
  {
    try
      {
        return json_decode($json, true, 512, JSON_THROW_ON_ERROR);
      }
    catch(JsonException $e)
      {
        throw new ImportException("Les paramètres ne peuvent pas être chargés", 0, $e);
      }
  }
try
  {
    loadSettings("{invalid json}");
  }
catch(ImportException $e)
  {
    echo $e->getMessage()."\n";
    echo "cause : ".get_class($e->getPrevious()).": ".$e->getPrevious()->getMessage();
  }
Les paramètres ne peuvent pas être chargés
cause : JsonException: Syntax error

Exceptions non interceptées

Une exception que personne n’intercepte termine le script par le message Fatal error: Uncaught …. Sur le site en production, les visiteurs ne doivent pas voir ce message. Le traitement pour tout le site se définit par set_exception_handler() :

set_exception_handler(function(Throwable $e)
  {
    error_log(get_class($e).": ".$e->getMessage()." in ".$e->getFile().":".$e->getLine());
    http_response_code(500);
    echo "Désolé, une erreur s'est produite. Réessayez plus tard.";
  });
throw new RuntimeException("La base de données ne répond pas");

Les détails de l’erreur vont dans le journal, le visiteur ne voit qu’un message général. Tout le réglage des messages d’erreur est décrit dans l’article Messages d’erreur en production.

Règles

  • Interceptez une exception là où vous pouvez en faire quelque chose (afficher l’erreur dans le formulaire, réessayer, renvoyer 404). Ailleurs, laissez-la continuer sa route.
  • Un catch vide, qui ne fait qu’avaler l’erreur, est la pire possibilité – l’erreur disparaît sans laisser de trace.
  • N’utilisez pas les exceptions pour les embranchements ordinaires du programme. Un utilisateur qui n’a pas rempli un champ est une situation normale, pas une exception.
  • PHP 8 ajoute throw à l’intérieur des expressions et catch sans variable, voir Arguments nommés et opérateur nullsafe.

Articles connexes :
Messages d’erreur en production
Changements de PHP 8 qui cassent l’ancien code
Espaces de noms (namespace)