Je dois dire que sur le coup, je n'ai pas été surpris.

En effet, je sais par expérience que mon code fait souvent appel à des fonctions avancées de PHP qui change parfois de comportement d'une version à l'autre du langage.

C'est pour cela que j'attend en général un bon mois avant de faire une mise à jour, suite à une ou deux mésaventures de ce type par le passé.

Cependant, je ne m'attendais pas à une erreur aussi visible, violente et brutale, mais plutôt à un problème beaucoup plus insidieux et difficile à détecter.

Qu'à cela ne tienne, j'ai tout de même pris le taureau par les cornes et j'ai commencé mon investigation.

Je suis tout d'abord allé voir les fichiers de log de mon serveur web, qui ne contenait absolument aucune erreur.

Je me suis ensuite directement tourné vers le coupable évident, j'ai nommé l'autoload, puisque c'est ce dernier qui est chargé d'inclure à la demande les classes d'objet dont le code à besoin pour fonctionner.

Pour des raisons historiques, l'autoload de ce site à un fonctionnement particulier.

En effet, il y a potentiellement plus d'une classe par fichier.

L'autoload commence donc rechercher un fichier portant le nom de la classe, et s'il ne le trouve pas, il lance une indexation du contenu de chaque fichier pour détecter les classes et le chemin d'accès qui leur correspond.

Afin de pas refaire cette indexation à chaque requête, son résultat est stocké par le système de fichier sous la forme d'un tableau associatif sérialisé .

Evidement, loi de murphy aidant, la classe manquante était dans un fichier contenant plusieurs classes et c'était donc le mécanisme d'indexation des classes, soit le code le plus complexe, qui posait problème.

Il fait en effet appel aux outils fournis par la SPL pour parcourir récursivement les répertoires contenant les fichiers de classe et les filtrer.

En résumé, le code ressemble de très loin à cela :

class Site_Autoload_File_Filter extends filterIterator {
const suffix = '.inc.php';

function __construct($path) {
parent::__construct(new recursiveIteratorIterator(new RecursiveDirectoryIterator($path)));
}

function accept() {
return (is_readable($this->getPathName()) and substr($this->getPathName(), - strlen(self::suffix)) === self::suffix);
}
}

foreach (new Site_Autoload_File_Filter(self::$classes_directory) as $inode) {
$file = file_get_contents($inode->getPathName());

if (preg_match_all('/^(?:abstract\s+|final\s+)?class\s+(\w+)\s*/m', $file, $matches, PREG_SET_ORDER) > 0) {
foreach ($matches as $match) {
$classes[$match[1]] = $inode->getPathName();
}
}
}

J'ai donc commencé par tracer l'éxécution de ce code et je me suis rendu compte que l'itérateur ne faisait pas du tout son travail puisqu'il qu'il ne parcourait pas du tout l'arborescence.

Il était donc difficile pour l''autoload d'inclure la classe demandée puisqu'il ne connaissait pas son emplacement.

Paradoxalement, le répertoire qu'il devait parcourir était tout à fait lisible au niveau des droits du système de fichiers.

Je dois avouer qu'à ce moment, j'ai tout de suite incriminé PHP 5.2.9.

Cependant, un examen du code de la SPL et un tour sur bugs.php.net m'ont vite fait changer d'avis, puisque le code n'avait pas subit de modification entre les deux versions et qu'aucun bug de ce type n'avait été remonté depuis la sorte de PHP 5.2.9.

J'ai donc extrait le code incriminé et je l'ai éxécuté à partir d'un fichier indépendant du site et en ligne de commande, et à ma grande surprise, l'itérateur a fait parfaitement son travail en parcourant récursivement tout les dossiers.

Je dois dire que j'ai été extrémement déstabilisé, puisqu'à code exactement identique, l'éxécution produisait un résultat complétement différent suivant l'interface utilisée.

Exécution identique ? non, pas vraiment !

En effet, en ligne de commande, le script était éxécuté avec mes droits d'utilisateur, alors que le code du site était éxécuté avec les droits de mon serveur web, potentiellement différents.

Du coup, j'ai modifié mon arborescence pour avoir des droits identiques à ceux de mon serveur, et j'ai notament interdit à mon utilisateur de pouvoir lire les répertoires .svn contenus dans mon arborescence, puisque mon serveur web ne pouvait pas y accéder.

Et là, miracle !

En effet, l'éxécution de mon script en ligne de commande a généré une magnifique exception m'expliquant que le script ne pouvait pas parcourir l'arborescence que je lui indiquait car il ne disposait pas des droits nécéssaires pour lire l'un des répertoires qu'il contenait, en l'occurence un .svn.

Mais pourquoi donc cette exception, somme toute logique, n'était pas attrapée au niveau de mon site et gérée soit pour être affichée, soit pour étre logguée ?

Un détour par le gestionnaire d'exception du site ne m'a pas tellement aidé, puisque ce dernier ne faisait absolument rien qui pouvait expliquer pourquoi l'exception disparaissait dans les méandres de la mémoire de ma machine.

En désespoir de cause, je suis aller voir la documentation relative à l'autoload, et j'ai passé ensuite les 5 minutes suivantes à me flageller.

En effet, les exceptions émises depuis ce dernier ne peuvent pas être attrapées !

Du coup, le code suivant a résolu mon problème :

try
foreach (new Site_Autoload_File_Filter(self::$classes_directory) as $inode) {
$file = file_get_contents($inode->getPathName());

if (preg_match_all('/^(?:abstract\s+|final\s+)?class\s+(\w+)\s*/m', $file, $matches, PREG_SET_ORDER) > 0) {
foreach ($matches as $match) {
$classes[$match[1]] = $inode->getPathName();
}
}
}
} catch (exception $exception) {
trigger_error($exception->getMessage(), E_USER_ERROR);
}

Au total, j'aurai perdu avec ce problème quasiement une journée.

Cependant, j'en ai tiré un certain nombre de leçon :

  1. PHP, c'est vraiment de la merde ! Ne pas pouvoir attraper une exception quelque soit son lieu d'émission est vraiment une bêtise énorme, d'autant qu'il n'y a même pas un message d'erreur ou un log de généré pour en avertir le programmeur.
  2. Il faut toujours reproduire un problème et chercher sa solution dans le contexte dans lequel il se produit.
  3. RTFM !
  4. Il ne faut pas modifier plusieurs choses à la fois sur son environnement de travail sous peine de ne pas pouvoir définir rapidement le responsable des problèmes.

Et pour ceux qui se poserait la question, le problème n'était pas apparu jusqu'à maintenant puisque le cache du moteur d'indexation était utilisé, cache que j'ai effacé de mon système de fichiers en le supprimant du dépôt du système de gestion de version.