WordPress
2 septembre 2026

Deux modules PrestaShop, une même librairie : le 500 qui tombe une fois sur deux

Un 500 intermittent est ce qui se rapproche le plus d’un cauchemar de développeur. Deux à trois fois sur cinq après une purge de cache, la page tombe. Les autres fois, elle passe. Aucun changement de code entre les deux essais.

Nous avons eu ce cas récemment, avec une particularité supplémentaire : l’erreur ne se produisait que sur une seule boutique d’une installation multiboutique. L’autre répondait en 200, cinq fois sur cinq.

L’histoire vaut d’être racontée parce que la cause racine n’est pas un bug dans un module. C’est une propriété structurelle de l’écosystème PrestaShop, que personne ne documente et à laquelle tout le monde est exposé.

Le symptôme

L’exception, quand on finit par la capturer, est parfaitement claire :

ServiceNotFoundException: You have requested a non-existent service
"...\Infrastructure\Validator\FrontControllerValidator"

Elle est levée depuis le hook actionFrontControllerSetMedia d’un module de paiement. Un service demandé au conteneur, un conteneur qui répond qu’il ne le connaît pas.

Le premier réflexe est de chercher une erreur de configuration. C’est là que ça devient intéressant.

Première anomalie : un service déclaré mais absent

Le service est bien déclaré dans le fichier de configuration du module :

services:
  ...\Infrastructure\Validator\FrontControllerValidator:
    ...

Mais la classe correspondante n’existe nulle part dans le répertoire src/ du module. Le module n’a pas non plus de composer.json à sa racine. Le paquet livré déclare donc un service dont l’implémentation n’a jamais été embarquée.

À ce stade, on tient une explication. Elle est fausse, ou plutôt incomplète : si c’était toute l’histoire, l’erreur serait systématique. Or elle tombe une fois sur deux.

Deuxième anomalie : ce n’est pas le bon conteneur

C’est en lisant la trace d’appel ligne par ligne que le vrai sujet apparaît. La librairie de conteneur de services qui lève l’exception ne vient pas du module de paiement. Elle vient du répertoire vendor/ d’un autre module, un module marketing sans aucun rapport :

modules/<module-marketing>/vendor/prestashop/module-lib-service-container/

Les deux modules embarquent la même librairie, prestashop/module-lib-service-container, chacun dans son propre vendor/. Et c’est celle du module marketing qui a gagné.

Pourquoi ça arrive, et pourquoi c’est structurel

PrestaShop n’a aucune résolution de dépendances entre modules. Chaque module est un paquet autonome qui embarque ses propres librairies dans son vendor/, avec son propre autoloader Composer généré à part.

Or l’autoloader PHP fonctionne au premier arrivé. Quand une classe Foo\Bar est demandée, c’est le premier autoloader enregistré capable de la résoudre qui la charge. Si deux modules embarquent Foo\Bar dans deux versions différentes, la version chargée est celle du module dont l’autoloader a été enregistré en premier.

Et l’ordre d’enregistrement dépend de l’ordre d’exécution des hooks, qui dépend lui-même des positions configurées, des modules actifs sur la boutique courante, et de la page demandée.

Ce n’est pas un oubli de configuration, c’est le fonctionnement nominal. Le cœur de PrestaShop ne charge aucun vendor/ de module : la classe Module n’en fait jamais mention. Chaque module qui a des dépendances appelle donc lui-même son autoloader, généralement depuis son fichier d’entrée :

// Dans monmodule.php, avant la déclaration de la classe
require_once __DIR__ . '/vendor/autoload.php';

Autant de fichiers d’entrée exécutés, autant d’autoloaders empilés, et aucune autorité pour arbitrer entre eux.

La librairie de notre incident illustre bien le risque. prestashop/module-lib-service-container est publiée par PrestaShop et embarquée par de nombreux modules officiels. Ses versions vont de 1.0.0 à v2.0, en passant par 1.1.0, 1.2.0, 1.3.0, 1.3.1 et 1.4.0. Deux modules installés à deux ans d’intervalle n’embarquent donc pas le même code, et rien ne les en empêche.

Vous avez donc un système où la version d’une librairie chargée en mémoire dépend de la page que le visiteur consulte. Quand les deux versions sont compatibles, personne ne le remarque jamais. Quand elles divergent sur un point précis, vous obtenez un plantage qui dépend du contexte, ce qui est la définition même du bug intermittent.

Une fois cette mécanique comprise, le caractère aléatoire cesse d’être mystérieux. Ce n’était pas aléatoire, c’était contextuel. Nous cherchions une cause dans le module qui plantait, alors que la cause était dans un module qui n’avait rien à voir.

Et le mystère de la seule boutique concernée

Restait à expliquer pourquoi une seule des deux boutiques tombait.

Le hook du module de paiement commence par un garde : il ne fait rien si le marchand n’est pas correctement configuré. Sur la seconde boutique, ce garde aurait dû arrêter l’exécution immédiatement, puisque cette boutique n’avait jamais été rattachée à un compte marchand.

Sauf qu’une clé de configuration avait été créée à l’exécution pour cette seconde boutique, en reprenant le même identifiant de compte que la première. La seconde boutique héritait donc de l’identité de la première, passait le garde, et allait chercher un service inexistant dans un conteneur qui n’était pas le sien.

Trois anomalies indépendantes, dont aucune ne suffisait à provoquer le plantage seule. C’est assez typique : les incidents durs à diagnostiquer sont rarement causés par une erreur, mais par une conjonction d’approximations qui se neutralisent la plupart du temps.

Comment diagnostiquer ce type de cas

Trois réflexes, dans l’ordre.

Lisez le chemin des fichiers dans la trace d’appel, pas seulement les noms de classes. C’est le seul endroit où la collision est visible. Une classe PrestaShop\ModuleLibServiceContainer\... ne vous dit pas de quel vendor/ elle vient. Le chemin du fichier, si.

Cherchez les librairies partagées. La commande tient sur une ligne et vous donne immédiatement la carte des collisions potentielles :

find modules -maxdepth 4 -type d -path "*/vendor/*" -name "module-lib-*" | sort

Mais la commande la plus utile est ailleurs. Elle ne cherche pas les collisions possibles, elle vous dit quel fichier a réellement gagné pendant l’exécution. Deux lignes suffisent, posées temporairement dans le hook qui plante :

// Quel fichier a réellement fourni cette classe ?
$ref = new \ReflectionClass(\PrestaShop\ModuleLibServiceContainer\DependencyInjection\ServiceContainer::class);
$this->logger->warning('Classe chargée depuis', ['fichier' => $ref->getFileName()]);

// Et dans quel ordre les autoloaders se sont enregistrés
foreach (spl_autoload_functions() as $i => $loader) {
    if (is_array($loader) && is_object($loader[0])) {
        $this->logger->warning('Autoloader', [
            'rang' => $i,
            'source' => (new \ReflectionClass($loader[0]))->getFileName(),
        ]);
    }
}

Le premier appel imprime le chemin absolu du fichier réellement chargé, et c’est ce chemin qui trahit le vendor/ fautif. Le second liste les autoloaders dans leur ordre d’enregistrement, ce qui donne directement l’explication du « pourquoi celui-là et pas l’autre ». Relancez la page deux fois, une où elle passe et une où elle plante : si le chemin change entre les deux, vous n’avez plus rien à chercher.

Élargissez ensuite à toutes les dépendances, pas seulement les librairies PrestaShop. Guzzle, Monolog et Symfony sont les grands classiques de la collision, parce que tout le monde les embarque. C’est la même famille de problème que les mauvaises pratiques sur les hooks : un comportement parfaitement documenté côté PHP, mais dont personne ne mesure l’effet à l’échelle d’une boutique réelle.

Face à un bug intermittent, cherchez ce qui change entre deux exécutions. Si le code est identique, alors c’est l’état qui diffère : l’ordre de chargement, le contenu du cache, la boutique courante, la page demandée. L’intermittence n’est pas du hasard, c’est une variable que vous n’avez pas encore identifiée.

Ce qu’on peut en faire

Soyons honnêtes sur les limites : il n’existe pas de correctif propre côté intégrateur. Vous ne pouvez pas dédupliquer les vendor/ de modules tiers sans casser leurs mises à jour, et vous ne contrôlez pas ce que leurs éditeurs embarquent.

Ce qui reste à votre portée :

Désactiver ce qui n’a pas lieu d’être. Dans notre cas, le module de paiement n’avait aucune raison d’être actif sur la seconde boutique, qui n’a pas de compte marchand. Le désactiver là où il ne sert pas supprime le problème et allège accessoirement le temps de réponse.

Traiter les librairies partagées comme une dette connue. Faire l’inventaire au moment de choisir un module, au même titre qu’on regarde sa compatibilité de version. Deux modules qui embarquent la même librairie dans des versions éloignées, c’est un risque à documenter avant l’installation, pas après le premier incident.

Ne jamais conclure trop vite sur un module coupable. Le module qui plante est souvent la victime, pas la cause. Dans notre cas, le module de paiement se comportait normalement compte tenu de ce qu’il avait reçu.

Et pour vos propres modules, la règle est simple : n’embarquez dans vendor/ que ce dont vous avez réellement besoin, et préférez une dépendance directe à une librairie tierce quand le besoin tient en trente lignes. Chaque paquet ajouté est un candidat à la collision, avec un module que vous ne connaissez pas encore, sur une page que vous n’avez pas testée.

Nous vous recommandons aussi