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 :
- Soit le code inclus génère une erreur ;
- 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.
14 réactions
1 De oxman - 31/10/2011, 12:09
Autant d'habitude je suis agréablement surpris/convaincu de ce que tu montres, autant là, c'est l'artillerie lourde pour pas grand chose et je ne trouve pas ça super de mettre/enlever un gestionnaire d'erreur pour un include.
Juste le fait de tester l'existence du fichier est largement suffisant dans une très grande majorité des cas.
2 De Da Scritch - 31/10/2011, 12:17
Ce qui veut dire que si on a déjà écrit un error_handler, on est marron pour l'include ?
3 De mageekguy - 31/10/2011, 13:39
@Da Scritch : tu peux récupérer le gestionnaire d'erreur précédemment défini et l'utiliser dans la fermeture lexicale à la place du
return false;
.4 De Ben - 31/10/2011, 13:51
Y a pas moyen de gérer ca en posant simplement un verrou (flock) sur le fichier a inclure pendant qu'on fait l'include et le relacher apres, non?
5 De Steuf - 31/10/2011, 13:54
@oxman :
Je suis assez d'accord il est très rare de faire de l’include sur du code PHP généré à la volé, ou un fichier généré à la volé et donc le cas de compétition est inexistant dans 99,9% des cas et dans ces 0.1% ça reste très peu probable. La solution la plus cohérente reste de faire passer toutes les erreurs en exception, comme dirait l'autre on fait de l'objet ou on en fait pas et on évite de s’asseoir le cul entre deux chaises.
Et puis bon au niveau des performances, ça me parait coûteux pour pas grand chose pour un cas très très particulier. En gros je dirait code sympa pour l'"intelect" mais à ne vraiment pas utiliser à tord et à travers.
6 De mageekguy - 31/10/2011, 13:57
@Ben : C'est une alternative jouable, la seule chose à penser est qu'il faut potentiellement déterminer le chemin d'accès exact au fichier en tenant compte de
include_path
pour pouvoir le verrouiller.Le verrou induira de plus une attente si le fichier à inclure est déjà verrouiller par un autre processus, le temps que ce dernier le libère, ce qui peut être un effet de bord gênant.
7 De mageekguy - 31/10/2011, 14:01
@Steuf : Pour le cas de compétition, tout dépend du contexte.
En environnement multi-processus, si le fichier à inclure est partagé entre tous les processus et que l'un d'entre eux est susceptible de l'effacer, la probabilité de se retrouver en situation de compétition est bien plus grande que 0.1%.
De plus, l'idée n'est pas de remplacer les appels classiques à
include
/include_once
, mais par exemple de gérer l'inclusion de fichiers de configuration écrit en PHP, par exemple, comme ceux de atoum, ou bien encore générés dynamiquement par d'autres processus.Et intuitivement, je ne suis vraiment pas convaincu que ce code ait un impact réellement significatif sur les performances.
8 De Steuf - 31/10/2011, 14:16
@mageekguy:
Qu'on soit d'accord le 0.1% c'est le fameuse chance d'être dans ce contexte précis et dans ce contexte il y a une certaine probabilité de concurrence En gros ce que je veux dire c’est que le code n'est pas inintéressant mais à ne pas utiliser bêtement pour les includes que l'on fait, ici je veux juste bien mettre en avant que ce code n'est utile que dans un cas de figure qui est très rarement utilisé en PHP Et puis bon dans le cas de forte concurrence sur un fichier j'utilise d'instinct un verrou (Bon sur un NFS c'est plus compliqué mais c’est encore une fois un cas rare :)). Après en mode CLI dès qu'il me faut du "multithread" je bascule directement sur du Java (Parce que bon SPL c’est gentil mais c'est un peu de la bidouille :p et niveau performance c'est pas extra). Enfin bref je pense que sur le fond on est d'accord juste que je voulais mettre en garde ceux qui voudraient utiliser ce code n'importe comment
9 De Geoffray - 31/10/2011, 14:18
Certaines classes du Zend Framework gèrent les erreurs de cette manière, ce n'est donc apriori pas un si mauvais choix. J'ai quand même quelques suggestions :
1) Pourquoi ne pas simplement tester le message d'erreur généré par PHP au lieu de parcourir tout le tableau des fichiers inclus ? Ca me semble moins lourd, et plus besoin de la fermeture lexicale.
2) Si le fichier inclus génère 10000 notices, ton error_handler va être appelé autant de fois... Pourquoi ne pas le limiter aux warnings ?
Je propose donc quelque chose du genre :
<?php
function includeFile($path)
{
set_error_handler(function($errno , $errstr, $errfile, $errline, $errcontext){
if (preg_match('`include.+: failed to open stream:`', $errstr) > 0) {
throw new Exception($errstr);
}
}, E_WARNING);
include_once $path;
}
10 De mageekguy - 31/10/2011, 14:38
@Geoffray : Et le jour ou PHP changera le format de ces erreur, tu seras bon pour modifier ton code.
Ce n'est clairement pas ma philosophie.
Et un gestionnaire d'erreurs, c'est juste une fonction, et c'est le principe même d'une fonction d'être appelée un grand nombre de fois.
Encore une fois, le but n'est pas de remplacer les appels traditionnels à
include
/include_once
.11 De paul - 01/11/2011, 15:25
J'avoue qu'il m'est arrivé ce cas de figure, surtout quand je faisais du "cache" dans un fichier.
Lors du rechargement du fichier cache à un moment le fichier n'existe plus.
A l'époque (ca date de quelques années) j'avais ajouté (de mémoire) un marqueur à la fin du fichier cache (à la fin j'avais rajouté un $load = true;) ce qui donnait
@include($file);
if(!isset($load) || ! $load ){
...
}
12 De mageekguy - 02/11/2011, 07:40
@paul : Pour info, tu aurais pu mettre un
return true;
à la fin du fichier inclus et faire un(if (@include('toFichier') === false)
.Cette stratégie n'est pas jouable dans mon cas de figure car je ne veux pas que l'utilisateur ait des contraintes au niveau des fichiers de configuration.
13 De Paul - 03/11/2011, 13:16
@mageekguy: Tiens j'avais jamais lu la doc de include en détails ...
Mais je viens de lire :
"Si le fichier ne peut être inclus, FALSE est retourné et une erreur de niveau E_WARNING est envoyée."
Ca ne peux pas résoudre ton probleme ?
if( @include('tonfichier') === false ){ thrown new Exception(); }
14 De mageekguy - 03/11/2011, 20:21
@Paul : Non car en cas d'erreur dans le code du fichier inclus, le @ la masquera.