Ma première idée a été d'utiliser l'opérateur de contrôle d'erreur @ sur l'instruction concernée afin que l'erreur de type WARNING générée par cette dernière ne soit pas gérée par le gestionnaire d'erreur installé, puis ensuite de vérifier que le fichier demandé existait bien parmi ceux renvoyés par la fonction get_included_files().

<?php
...
public function includeFile($path)
{
   @include_once $path;

   if (in_array($path, get_included_files()) === false)
   {
      throw new exceptions\runtime\file('Unable to include file \'' . $path . '\'');
   }
}
...
?>

Cela semblait relativement bien marché, au détail près que si le fichier inclus contenait des erreurs, par exemple à cause d'un appel à require sur un fichier inexistant, l'exécution du script s'arrêtait sans que l'utilisateur n'ait le moindre message d'erreur lui en expliquant la raison.

En effet, l'opérateur @ s'applique non seulement à l'instruction sur laquelle il est appliqué, mais aussi à l'intégralité du code appelé par cette instruction.

En clair, ma solution était de la merde, et j'ai donc été obligé d'en chercher une autre.

Dans le cas d'un appel à include ou include_once, le gestionnaire d'erreur peut être appelé pour deux raisons :

  1. Soit le code inclus génère une erreur ;
  2. Soit le fichier demandé n'existe pas ;

J'ai donc eu l'idée de définir un gestionnaire d'erreur juste avant mon appel à include_once qui lance une exception uniquement si le fichier n'a pas été inclus et qui dans le cas contraire, demande au gestionnaire d'erreur par défaut d'intervenir.

Et une fois l'appel à include_once effectué, je restaure le gestionnaire d'erreur précédent à l'aide de la fonction restore_error_handler().

<?php
...
public function includeFile($path)
{
   set_error_handler(function($error, $message, $file, $line) use ($path) {
         $pathLength = strlen($path);

         foreach (get_included_files() as $includedFile)
         {
            if (strrpos($includedFile, $path) + $pathLength === strlen($includedFile))
            {
               return false; // appel au gestionnaire d'erreur par défaut
            }
         }

         throw new exceptions\runtime\file('Unable to include \'' . $path . '\'');
      }
   );

   include_once $path;

   restore_error_handler();
}
...
?>

Toute l'astuce consiste ici à utiliser une fermeture lexicale, définie par use ($path) pour injecter dans le gestionnaire d'erreur le chemin du fichier devant être inclus et pouvoir ainsi tester qu'il l'a bien été.

Au passage, j'en ai profité pour modifier le code afin qu'il prenne en compte le fait que le fichier peut avoir été inclus à partir de l'un des répertoires définis par la directive de configuraiton include_path lors de la vérification du fait qu'il a bien été inclus.