Les tutoriels « créer son premier module PrestaShop » se ressemblent tous, et la documentation officielle fait le travail mieux que n’importe quel article. Le squelette d’un module tient en une page, et il est en bas de celle-ci.
Ce qui n’est écrit nulle part, en revanche, c’est pourquoi ce module parfaitement correct n’apparaît pas dans la liste des modules. Pas de message, pas de log, pas d’erreur PHP. Il n’est simplement pas là.
PrestaShop applique quatre règles au chargement d’un module, et il les applique en silence. Les voici, telles qu’elles sont écrites dans le cœur en 9.0.0.
Au programme :
- Les quatre règles silencieuses qui décident si votre module existe
- Pourquoi le même défaut vous alerte en local et se tait en production
- Le piège de l’autoload, qui touche surtout les développeurs expérimentés
- Le squelette minimal, à jour des versions réellement requises
Les quatre règles silencieuses
Tout part de la méthode qui charge un module par son nom :
// classes/module/Module.php (PrestaShop 9.0.0)
public static function getInstanceByName($module_name)
{
if (!Validate::isModuleName($module_name)) {
if (!_PS_MODE_DEV_) {
return false;
}
die(Tools::displayError(/* ... %1$s is not a valid module name. ... */));
}
if (!isset(static::$_INSTANCE[$module_name])) {
if (!Tools::file_exists_no_cache(_PS_MODULE_DIR_ . $module_name . '/' . $module_name . '.php')) {
return false;
}
return Module::coreLoadModule($module_name);
}
return static::$_INSTANCE[$module_name];
}
Règle 1 : le nom doit passer une expression régulière
La validation est plus permissive qu’on ne le dit habituellement :
// classes/Validate.php
public static function isModuleName($module_name)
{
return is_string($module_name) && preg_match('/^[a-zA-Z0-9_-]+$/', $module_name);
}
Les majuscules sont acceptées, les tirets aussi. Ce qui est refusé, ce sont les points, les espaces, les accents et tout le reste. La convention du tout-minuscule reste une bonne pratique de lisibilité, mais ce n’est pas ce que le cœur vérifie. En revanche, un nom contenant un point, ce qui arrive quand on nomme un dossier d’après un fichier, échoue à cette étape.
Règle 2 : le même défaut se comporte différemment selon l’environnement
C’est le détail le plus important de tout l’extrait, et il mérite d’être relu :
if (!Validate::isModuleName($module_name)) {
if (!_PS_MODE_DEV_) {
return false; // production : silence total
}
die(...); // développement : message explicite
}
Sur votre machine, avec le mode développement actif, PrestaShop vous dit exactement ce qui ne va pas et s’arrête. En production, il renvoie false et continue comme si de rien n’était.
La conséquence pratique dépasse largement le cas du premier module : un module qui fonctionne en local et disparaît en production n’est pas forcément un problème de déploiement, ce peut être exactement le même défaut, simplement rendu muet. C’est aussi une bonne raison de ne pas déboguer un souci de module directement sur le serveur de production, où l’information est volontairement supprimée.
Règle 3 : le fichier doit porter le nom du dossier
modules/
└── monmodule/
└── monmodule.php ← le chemin exact que le cœur vérifie
Le cœur teste _PS_MODULE_DIR_ . $module_name . '/' . $module_name . '.php'. Un dossier monmodule contenant index.php, main.php ou MonModule.php sur un système de fichiers sensible à la casse renvoie false, sans message. C’est la cause numéro un des modules invisibles après un transfert entre un poste de développement macOS, insensible à la casse, et un serveur Linux qui, lui, l’est.
Règle 4 : la classe doit exister sans autoload
Celle-là piège surtout les développeurs expérimentés, précisément parce qu’ils font les choses proprement :
protected static function coreLoadModule($module_name)
{
include_once _PS_MODULE_DIR_ . $module_name . '/' . $module_name . '.php';
// ... prise en charge d'un éventuel override ...
if (!$r && class_exists($module_name, false)) {
$r = static::$_INSTANCE[$module_name] = ServiceLocator::get($module_name);
}
return $r;
}
Le second argument de class_exists() vaut false, ce qui désactive l’autoloading. La classe doit donc être déclarée par l’inclusion du fichier principal lui-même, et non chargée par Composer.
Autrement dit, l’architecture qui paraît la plus propre, à savoir mettre la classe principale sous src/ avec du PSR-4 et ne laisser qu’un fichier de façade à la racine, ne fonctionne pas. Le fichier monmodule.php doit contenir la déclaration class MonModule extends Module, ou l’inclure explicitement. Le reste du module peut parfaitement vivre en PSR-4, mais pas la classe d’entrée.
Notez aussi que le nom de classe est comparé au nom du dossier, et que PHP est insensible à la casse sur les noms de classes : MonModule, Monmodule et MONMODULE conviennent tous pour un dossier monmodule.
Les prérequis réels
Les versions annoncées dans les tutoriels sont souvent celles d’une génération précédente. Voici ce que déclarent les dépôts officiels :
// composer.json de PrestaShop, clé require.php
PrestaShop 9.0.0 → "php": ">=8.1"
PrestaShop 8.2.0 → "php": ">=7.2.5"
La différence n’est pas cosmétique. Si vous visez PrestaShop 9, vous disposez des types d’union, des propriétés en lecture seule, des énumérations et des attributs, et il n’y a aucune raison de vous en priver. Si vous devez couvrir les deux générations, c’est le plancher de la 8 qui s’impose à votre code.
C’est aussi ce que vous déclarez au cœur avec ps_versions_compliancy, et cette déclaration est vérifiée à l’installation : un module compatible affiché mais impossible à installer vient presque toujours de là.
Le squelette minimal
Une fois les quatre règles respectées, le fichier d’entrée tient en peu de choses :
<?php
declare(strict_types=1);
if (!defined('_PS_VERSION_')) {
exit;
}
class MonModule extends Module
{
public function __construct()
{
$this->name = 'monmodule'; // identique au dossier et au fichier
$this->tab = 'front_office_features';
$this->version = '1.0.0';
$this->author = 'Votre nom';
$this->need_instance = 0;
$this->bootstrap = true;
$this->ps_versions_compliancy = ['min' => '8.0.0', 'max' => _PS_VERSION_];
parent::__construct();
$this->displayName = $this->trans('Mon module', [], 'Modules.Monmodule.Admin');
$this->description = $this->trans('Description courte.', [], 'Modules.Monmodule.Admin');
}
public function install(): bool
{
return parent::install()
&& $this->registerHook('displayHome')
&& Configuration::updateValue('MONMODULE_ACTIF', false);
}
public function uninstall(): bool
{
return parent::uninstall()
&& Configuration::deleteByName('MONMODULE_ACTIF');
}
public function hookDisplayHome(array $params): string
{
return $this->fetch('module:monmodule/views/templates/hook/home.tpl');
}
}
Trois points de vigilance sur ce squelette. $this->name doit être strictement identique au nom du dossier, sinon l’installation écrit une ligne incohérente en base. La garde _PS_VERSION_ évite qu’un accès direct au fichier ne l’exécute hors contexte. Et uninstall() doit défaire tout ce que install() a fait, sans quoi une réinstallation part d’un état sale, ce qui produit des comportements très difficiles à reproduire.
Ce qui casse ensuite
Une fois le module visible et installé, le piège suivant concerne les hooks. Sur PrestaShop, le lien entre un module et un hook vit en base de données, écrit par registerHook() pendant l’installation. Ajouter une méthode hookXxx() dans une nouvelle version ne suffit donc pas : sans script de mise à jour qui appelle registerHook(), le code existe et ne s’exécute jamais.
C’est un comportement contre-intuitif quand on vient d’autres écosystèmes, et nous l’avons détaillé, avec ses conséquences sur l’ordre d’exécution, dans notre comparaison des modèles d’extension.
Au-delà, les deux réflexes qui font gagner le plus de temps sur un premier module sont d’éviter les surcharges du cœur, qui se cassent à chaque mise à jour, et de séparer la logique métier du fichier d’entrée. Le sujet est traité en détail dans les erreurs courantes sur les hooks.
Questions fréquentes
Mon module n’apparaît pas dans la liste, aucune erreur.
Vérifiez les quatre règles dans l’ordre : le nom passe-t-il l’expression /^[a-zA-Z0-9_-]+$/, le fichier s’appelle-t-il exactement comme le dossier, la classe est-elle déclarée dans ce fichier sans dépendre de l’autoload, et son nom correspond-il au dossier. Le cœur renvoie false à chacune de ces étapes sans rien journaliser.
Il fonctionne en local et pas en production.
Deux causes probables. Soit la casse : macOS ne distingue pas MonModule.php de monmodule.php, Linux si. Soit le mode développement, qui affiche un message explicite en local là où la production renvoie simplement false.
Puis-je mettre ma classe principale dans src/ avec du PSR-4 ?
Non, pas la classe d’entrée. Le cœur appelle class_exists($module_name, false), avec l’autoload désactivé. La classe doit être déclarée par l’inclusion du fichier principal. Le reste du module peut être en PSR-4 sans problème.
Quelle version de PHP viser ?
PrestaShop 9 exige PHP 8.1 au minimum, PrestaShop 8 se contente de 7.2.5. Si vous ne ciblez que la 9, servez-vous des apports du langage. Si vous couvrez les deux, c’est le plancher de la 8 qui s’impose.
Mon nouveau hook ne s’exécute pas après une mise à jour.
Le lien module-hook vit en base. Ajouter la méthode ne suffit pas, il faut un script de mise à jour qui appelle registerHook().
Faut-il un module ou une surcharge du cœur ?
Un module, toujours. Une surcharge se casse à chaque montée de version et n’est pas transférable d’une boutique à l’autre.
Votre module refuse d’apparaître et vous avez déjà tout relu trois fois ? Lancez la vérification des quatre règles dans l’ordre, c’est presque toujours la troisième ou la quatrième. Si ce n’est aucune des quatre, écrivez-nous.


