Un site e-commerce utilise quatre extensions distinctes qui, chacune de son côté, embarque sa propre copie de la bibliothèque HTTP Guzzle via Composer pour effectuer des appels vers des services externes : passerelle de paiement, transporteur, comparateur de prix, et service de vérification d’adresse. Après la mise à jour de l’une de ces quatre extensions, le site affiche un fatal error : Class "GuzzleHttp\Client" not found, alors que la classe est bel et bien présente sur le disque, dans le dossier vendor de l’extension récemment mise à jour.
Le symptôme
L’erreur ne se produit pas systématiquement, elle dépend de l’ordre dans lequel les quatre extensions sont chargées par WordPress, lui-même déterminé par l’ordre alphabétique de leur dossier, sauf réorganisation manuelle. Avant la mise à jour, l’ordre de chargement faisait que l’extension récemment mise à jour bénéficiait d’une version de Guzzle compatible avec les trois autres. Après la mise à jour, cette même extension embarque une version majeure plus récente de Guzzle, dont l’espace de noms interne a légèrement changé de structure, ce qui casse la compatibilité avec le code des trois autres extensions qui appellent encore l’ancienne structure de classes.
Le diagnostic

Le cœur du problème n’est pas Guzzle en tant que tel, mais la façon dont chaque extension déclare son autoloader Composer. Un schéma typique et problématique ressemble à ceci dans le fichier principal de chaque plugin concerné :
// Dans chacune des quatre extensions, sans coordination entre elles :
if ( file_exists( __DIR__ . '/vendor/autoload.php' ) ) {
require __DIR__ . '/vendor/autoload.php';
}
Chaque extension charge son propre autoloader Composer sans jamais vérifier si une classe du même espace de noms a déjà été enregistrée par une autre extension. Composer génère en interne un registre de classes basé sur l’espace de noms PSR-4, et selon l’ordre de chargement, c’est le premier autoloader chargé qui enregistre en premier l’espace de noms GuzzleHttp. Si cette première extension chargée embarque une version de Guzzle incompatible avec ce qu’attendent les extensions suivantes, ces dernières échouent, puisque PHP ne conserve qu’une seule définition par nom de classe complètement qualifié : impossible de charger deux versions différentes de GuzzleHttp\Client simultanément dans le même processus PHP.
Le correctif à court terme : vérifier avant de déclarer
if ( ! class_exists( 'GuzzleHttp\Client' ) ) {
require __DIR__ . '/vendor/autoload.php';
}
Ce garde-fou minimal évite au moins l’écrasement d’une classe déjà chargée par une autre extension, mais il ne résout pas le fond du problème : si l’extension qui a chargé la première embarque une version trop ancienne de Guzzle, incompatible avec les appels de méthode attendus par une autre extension, celle-ci échouera quand même, mais avec une erreur différente, un appel de méthode inexistante plutôt qu’une classe introuvable.
Le vrai correctif : préfixer les dépendances
La solution robuste, déjà largement pratiquée dans l’écosystème des extensions WordPress distribuées à grande échelle, consiste à préfixer entièrement l’espace de noms des dépendances tierces embarquées, à l’aide d’un outil comme Strauss ou PHP-Scoper, pour que chaque extension embarque sa propre copie totalement isolée, sous un espace de noms unique qui ne peut jamais entrer en collision avec celui d’une autre extension :
// Après passage par Strauss, dans l'extension A :
use MonExtensionA\Vendor\GuzzleHttp\Client;
// Dans l'extension B, une copie totalement distincte :
use MonExtensionB\Vendor\GuzzleHttp\Client;
Avec ce préfixage, les quatre extensions de notre exemple peuvent chacune embarquer sa propre version de Guzzle, potentiellement différente d’une extension à l’autre, sans jamais entrer en conflit, puisque chaque copie vit sous un espace de noms distinct et totalement isolé du point de vue de PHP.
Étapes pour appliquer ce correctif à une extension existante
- Installer Strauss en tant que dépendance de développement via Composer, sans l’embarquer dans la distribution finale.
- Configurer, dans le fichier
composer.json, le préfixe de namespace cible et le dossier de sortie souhaité pour les dépendances préfixées. - Exécuter Strauss dans le pipeline de build avant chaque publication d’une nouvelle version de l’extension, jamais manuellement à la main au risque d’oublis.
- Mettre à jour tous les appels de code de l’extension pour utiliser le nouvel espace de noms préfixé plutôt que l’espace de noms d’origine de la bibliothèque.
Ce qu’il faut retenir pour diagnostiquer rapidement ce type d’erreur
- Une erreur « Class not found » qui apparaît après une mise à jour, sans modification du code métier de l’extension elle-même, doit immédiatement faire suspecter un conflit d’autoloading Composer entre extensions.
- Vérifier, via un outil comme Query Monitor ou simplement en inspectant les dossiers
vendorde chaque extension active, si plusieurs plugins embarquent la même bibliothèque tierce sans préfixage. - Reproduire le bug en changeant volontairement l’ordre d’activation des extensions confirme rapidement l’hypothèse d’un conflit d’ordre de chargement.
Deux extensions qui embarquent la même bibliothèque sans la préfixer ne coexistent pas, elles se disputent silencieusement le même espace de noms jusqu’à ce que l’une d’elles perde.
En résumé
Ce type de fatal error, déroutant parce qu’il apparaît après une mise à jour qui semble sans rapport, révèle presque toujours un conflit d’autoloading Composer entre plusieurs extensions qui embarquent la même dépendance sous le même espace de noms. Le préfixage systématique des dépendances tierces, via un outil dédié intégré au pipeline de build, reste la seule protection durable contre cette classe de bugs sur un site qui accumule plusieurs extensions issues d’auteurs différents.