Table des matières :
- Flux d’exécution d’un hook PrestaShop : ce que fait réellement Hook::exec
- Hooks dynamiques : quand le nom du hook est construit à l’exécution
- Où les regex entrent en jeu (et pourquoi ça finit parfois en dette technique)
- Implémenter un module robuste face aux hooks dynamiques (sans patcher le core)
- Déboguer les hooks dynamiques : visibilité, ordre d’exécution, et perf
- Sécurité et qualité : hooks = surface d’attaque + point de régression
Flux d’exécution d’un hook PrestaShop : ce que fait réellement Hook::exec
Contexte versions : les exemples ci-dessous sont valables pour PrestaShop 8.1.x et 9.x (cœur Symfony 6.4) avec PHP 8.1/8.2. Les signatures exactes et certains détails de cache peuvent varier selon la branche, donc vérifiez toujours le fichier classes/Hook.php de votre tag Git.
Un hook PrestaShop n’est pas « magique » : c’est une résolution (DB + cache) puis une boucle d’exécution sur les modules rattachés à un nom de hook. En pratique, Hook::exec($hookName, $params, ...) va (1) convertir le nom de hook en id_hook (table ps_hook), (2) récupérer la liste des modules attachés (table ps_hook_module, avec position pour l’ordre), (3) instancier le module si nécessaire, puis (4) invoquer une méthode du module (typiquement hookXxx) ou un rendu widget.
Pour rendre l’algorithme plus concret, voici ce qui fait souvent la différence entre « je comprends vaguement » et « je débogue en 5 minutes » :
- Résolution du hook : si
id_hookn’est pas résolu (hook absent), rien ne s’exécute et le rendu est vide (pour un hookdisplay*). - Filtrage par contexte : selon les options passées à
execet selon la version, PrestaShop peut vérifier les exceptions (pages où un module est exclu) et respecter la boutique (multiboutique). - Choix du point d’entrée module :
- si le module implémente le modèle « widget » (ex.
renderWidget()), le cœur privilégie ce chemin pour certains hooks d’affichage ; - sinon, il cherche une méthode
hook{NomDuHook}(ex.hookDisplayHeader). - Agrégation du résultat : sur les hooks d’affichage, le résultat est souvent concaténé (avec ou sans séparateur), donc un module lent ou instable impacte directement l’output final.
La partie « coût » est souvent sous-estimée : un hook d’affichage peut déclencher 10–40 modules sur une boutique réelle (tracking, consent, paiement, SEO, search, etc.). Comme la résolution se fait via le cache interne de PrestaShop mais que l’exécution reste du PHP utilisateur, la différence entre un hook « propre » (un return de HTML déjà caché) et un hook « sale » (requêtes SQL, HTTP, logique métier lourde) se mesure facilement en dizaines de millisecondes par page.
Mini-scénario réaliste (souvent observé sur des boutiques FR/EU) : displayHeader appelle un module de consentement + un tag manager + un module de review + un module de recherche interne. Si l’un d’eux fait un appel HTTP synchrone (API tiers) dans le hook ou reconstruit une config complexe sans cache, vous obtenez un ralentissement visible… et difficile à imputer si vous ne mesurez pas au niveau du hook. Pour une méthodologie de diagnostic côté code et SQL, voir : Optimisation de code PrestaShop : diagnostiquer lenteurs et requêtes SQL.
Enfin, ne confondez pas hook d’action et hook d’affichage. Les hooks action* servent à réagir à un événement (ex. actionObjectProductUpdateAfter) et retournent en général void (ou des structures si $array_return est utilisé). Les hooks display* retournent du HTML concaténé, ce qui implique des responsabilités supplémentaires : échappement, stabilité du markup, et gestion des dépendances front (CSS/JS).
Sur les hooks display*, deux règles « qualité » évitent beaucoup de dégâts :
- Toujours rendre le HTML déterministe (même structure, même wrappers), sinon vous créez des régressions CSS/JS difficiles à isoler.
- Externaliser l’ajout d’assets vers
actionFrontControllerSetMediaquand c’est pertinent, plutôt que d’injecter des<script>/<link>à la volée dans n’importe quel hook.
Côté performance front, le mauvais usage des hooks se cumule avec le cache Smarty/CCC/CDN ; si vous cherchez à rationaliser ce pipeline, voir : PrestaShop : optimiser performance via cache Smarty, CCC et CDN.
Hooks dynamiques : quand le nom du hook est construit à l’exécution
Dans PrestaShop, « hook dynamique » désigne surtout un fait brut : le nom du hook est une chaîne et peut être calculé (PHP), interpolé (Smarty) ou choisi conditionnellement (Twig). En PHP, c’est banal : Hook::exec('displayNav' . $i, $params) ; en Smarty : {hook h="displayNav{$i}"} ; en Twig (PrestaShop 9+), on rencontre des appels du type {{ renderhook('displayHeader') }} selon les intégrations.
Ce mécanisme est utilisé par les thèmes pour multiplier des emplacements sans dupliquer du code : displayNav1, displayNav2, displayFooterBefore, displayFooterAfter, etc. Point important : un hook dynamique n’existe pas “parce qu’on l’appelle”. Il doit exister en base (ps_hook) et avoir des modules attachés (ps_hook_module). Si votre template appelle displayNav3 mais que le hook n’existe pas, Hook::exec ne va pas « deviner » : vous aurez un rendu vide, et vous passerez du temps à déboguer un problème qui est en réalité un problème de configuration de positions.
Un moyen simple de valider ce point, sans hypothèse, est de vérifier la présence du hook et ses modules en base (utile aussi quand on suspecte un souci multiboutique) :
-- 1) Le hook existe-t-il ?
SELECT id_hook, name
FROM ps_hook
WHERE name = 'displayNav3';
-- 2) Quels modules sont greffés dessus, et dans quel ordre ?
SELECT hm.position, m.name AS module_name, hm.id_shop
FROM ps_hook_module hm
JOIN ps_module m ON m.id_module = hm.id_module
JOIN ps_hook h ON h.id_hook = hm.id_hook
WHERE h.name = 'displayNav3'
ORDER BY hm.position ASC;
Côté modules, la difficulté n’est pas de recevoir un hook (ça, PrestaShop sait faire), mais de ne pas exploser votre code quand il y a 5 variantes d’un même emplacement. Vous pouvez : (1) implémenter une méthode par hook (hookDisplayNav1, hookDisplayNav2, …), (2) implémenter une méthode générique (hookDisplayNav) et enregistrer le module sur chaque hook, ou (3) implémenter une logique de dispatch interne (souvent via regex) pour factoriser. La stratégie (2) est la plus lisible mais la plus verbeuse côté install()/uninstall().
Un piège classique : confondre « hooks dynamiques » et wildcards. PrestaShop ne fournit pas un registerHook('displayNav*'). Si vous voulez couvrir une famille (displayNav1..N), vous devez soit enregistrer explicitement les hooks, soit faire votre propre mécanisme de fallback (voir plus bas). Cette limitation du cœur est structurelle : la relation module↔hook est matérialisée en base ; la wildcard casserait l’ordonnancement, les exceptions, et la UI de positions.
Pour ancrer l’idée : dans une boutique avec un thème custom, il est fréquent qu’un intégrateur ajoute displayHome4 « sur le tas ». Sans création du hook et sans greffe dans le BO, votre module « ne bug pas », il n’est juste jamais invoqué. C’est l’une des raisons pour lesquelles un export/import de positions (ou une doc d’intégration) est précieux dans un contexte agence/équipes distribuées.
Pour la référence fonctionnelle côté PrestaShop (vocabulaire hooks, greffe, exceptions), la doc officielle est utile : Documentation officielle des hooks PrestaShop
Où les regex entrent en jeu (et pourquoi ça finit parfois en dette technique)
Les regex (PCRE) interviennent dès que vous voulez classifier des noms de hooks (ou de méthodes) sans écrire 15 if. Le cœur lui-même s’appuie régulièrement sur des conventions de nommage (displayXxx, actionXxx, actionObject{Model}{Verb}{When}), et côté module, on finit vite par vouloir extraire : un index (displayNav2 → 2), une “base” (displayNav2 → displayNav), ou un contexte (displayLeftColumnProduct → Product).
D’un point de vue outillage, rappelez-vous que les regex PHP reposent sur PCRE. Le manuel PHP résume proprement le contrat de base : « preg_match() returns 1 if the pattern matches the given subject, 0 if it does not, or false if an error occurred. » (PHP Manual, preg_match, preg_match()). Ce détail n’est pas cosmétique : en prod, une regex invalide renvoie false (erreur), et si vous traitez ça comme “pas de match”, vous masquez des bugs.
Deux bonnes pratiques qui évitent 80% des regex « fragiles » dans les modules PrestaShop :
- Ancrer systématiquement (
^...$) quand vous parsez un nom de hook/méthode : vous voulez valider une forme complète, pas trouver un sous-motif “quelque part”. - Limiter l’alphabet au strict nécessaire : si vos hooks sont alphanumériques + underscore, évitez
.*et préférez[A-Za-z0-9_]+(ou plus restrictif si vous contrôlez la convention).
Le problème concret sur PrestaShop n’est pas “les regex” en soi, mais les regex mal bornées appliquées sur des chaînes hautement variables, dans des chemins chauds (hooks exécutés à chaque page). Une regex trop permissive (/(.*)(\d+)/) peut déclencher du backtracking inutile, et une regex qui n’ancre pas (/^...$/) peut “matcher” des cas non prévus. Pour un dispatch sur des suffixes numériques, utilisez des patterns simples et bornés, par exemple :
// Exemple : extraire base + index d'un hook type displayNav12
if (preg_match('/^(?<base>[a-zA-Z_]+?)(?<idx>\d+)$/', $hookName, $m)) {
$base = $m['base'];
$idx = (int) $m['idx'];
}
Autre point non négociable : les regex dans les hooks d’affichage touchent le front. Si vous parsez des noms de hook pour injecter du JS/CSS (ex. “uniquement sur displayHeader*”), vous êtes à une mauvaise regex de charger des assets sur toutes les pages et de dégrader LCP/INP. Les impacts se voient directement sur les Core Web Vitals ; si vous travaillez déjà l’optimisation globale, croisez ça avec : PrestaShop performance : optimiser gros catalogues et Core Web Vitals.
Repère pratique (utile pour cadrer les discussions perf/SEO) : les seuils “Good” des Core Web Vitals sont généralement communiqués comme suit (Google / web.dev) :
| Indicateur | “Bon” (Good) | À surveiller… |
|---|---|---|
| LCP | ≤ 2,5 s | hooks displayHeader/displayTop qui chargent trop d’assets |
| INP | ≤ 200 ms | JS ajouté via hooks, tags tiers, listeners multiples |
| CLS | ≤ 0,1 | HTML injecté tard (footer, popin) sans réservation d’espace |
Même si vos hooks ne sont pas la cause unique, ils sont souvent un multiplicateur (plus de modules → plus de risques).
Implémenter un module robuste face aux hooks dynamiques (sans patcher le core)
Pré-requis avant de toucher un module en production : (1) avoir une pré-prod clonée, (2) pouvoir vider le cache (var/cache/* selon version), (3) un pipeline de tests minimal. Modifier le cœur pour “ajouter des wildcards” est presque toujours une mauvaise idée : vous perdez la maintenabilité et vous créez des divergences à chaque mise à jour (particulièrement sensible en migration vers PrestaShop 9, voir : PrestaShop 9 : adapter modules et thèmes aux contrôleurs services et Twig).
La stratégie la plus sûre consiste à enregistrer explicitement les hooks et à centraliser la logique dans une méthode interne. Exemple : vous voulez répondre à displayNav1 et displayNav2 de la même façon, mais avec une variation de template.
public function install(): bool
{
return parent::install()
&& $this->registerHook('displayNav1')
&& $this->registerHook('displayNav2');
}
public function hookDisplayNav1(array $params): string { return $this->renderNav($params, 1); }
public function hookDisplayNav2(array $params): string { return $this->renderNav($params, 2); }
private function renderNav(array $params, int $slot): string
{
$this->context->smarty->assign([
'slot' => $slot,
// ...
]);
return $this->fetch('module:monmodule/views/templates/hook/nav.tpl');
}
Cette approche a un avantage opérationnel : elle rend visible (dans le code) la liste exacte des hooks gérés, ce qui facilite :
- les audits de compatibilité thème (quels emplacements sont requis),
- la revue de sécurité (où sort le HTML),
- et le support (un hook = une méthode, donc un point d’entrée traçable).
Si vous voulez vraiment factoriser sans multiplier les méthodes, l’approche “dispatch” avec __call() est viable uniquement si le cœur utilise is_callable() (ce qui laisse la possibilité à __call d’être invoqué pour des méthodes inexistantes). C’est puissant mais plus opaque, donc à réserver aux modules internes d’équipe (ou documentés). Exemple minimal :
public function __call(string $name, array $arguments)
{
// Exemple : hookDisplayNav12 -> base=DisplayNav, idx=12
if (preg_match('/^hook(?<base>[A-Za-z_]+?)(?<idx>\d+)$/', $name, $m)) {
$params = $arguments[0] ?? [];
return $this->dispatchDynamicHook($m['base'], (int) $m['idx'], $params);
}
throw new BadMethodCallException(sprintf('Méthode %s inconnue', $name));
}
private function dispatchDynamicHook(string $base, int $idx, array $params): string
{
if ($base === 'DisplayNav') {
return $this->renderNav($params, $idx);
}
return '';
}
Ce type de pattern a deux limites nettes : (1) il peut masquer des erreurs de nommage de hook (vous “absorbez” tout), (2) il complique le support. À minima, loggez les hooks réellement déclenchés (en mode dev) et refusez explicitement les bases inconnues. Et n’oubliez pas le volet “positions” : si le hook n’est pas attaché au module en base, vous n’entrerez jamais dans votre regex.
Checklist “robustesse” (très concrète) pour des hooks dynamiques :
- [ ] Le module s’installe en greffant explicitement tous les hooks nécessaires (pas de dépendance implicite au thème).
- [ ] Le rendu varie uniquement via paramètres (slot/index), pas via duplication de logique métier.
- [ ] Les exceptions (pages exclues) sont documentées et testées (ex. ne pas injecter un script sur
order). - [ ] Cache applicatif (ex.
Cache::isStored()/Cache::store()ou cache propre module) pour éviter de recalculer un même HTML. - [ ] Fallback sûr : base inconnue → log + return vide (ou exception en
_PS_MODE_DEV_).
Déboguer les hooks dynamiques : visibilité, ordre d’exécution, et perf
Le debugging des hooks dynamiques commence par une question simple : quel hook est réellement appelé, et dans quel ordre ? Côté back-office, la page Design → Positions (ou son équivalent selon version) vous donne l’ordonnancement par position. Côté code, instrumentez localement Hook::exec via un override (à éviter en prod) ou, plus proprement, via votre module : loggez dans chaque hook critique le nom du hook et quelques marqueurs (contrôleur, shop, customer group).
Une source de confusion fréquente : un module peut être greffé mais “ne pas apparaître” car il est exclu via les exceptions (écran “Transplanter un module” / exceptions par contrôleur). Sur une boutique avec plusieurs front controllers (CMS, catégorie, produit, commande), on peut croire à un bug de hook dynamique alors que c’est juste une exception configurée.
Quand le problème est “le hook ne s’exécute pas”, les causes dominantes sont : (1) hook inexistant en DB, (2) module non greffé au hook (en base), (3) méthode absente ou non callable, (4) exception silencieuse si le site capture/ignore certains errors. Une manière rapide de trancher, avant même de déboguer PHP : vérifier en base l’attachement et l’ordre (cf. SQL plus haut), puis vérifier en BO que le module n’est pas désactivé pour la boutique/groupe.
Pour lever l’ambiguïté rapidement, un débogueur est plus efficace que 200 var_dump. Si vous êtes sur PHP 8.1/8.2, prenez le temps de configurer Xdebug correctement (CLI + FPM) : Xdebug : configurer le débogage PHP dans Visual Studio Code.
Sur la perf, le bon réflexe est de mesurer le coût réel des hooks les plus “bruyants” : displayHeader, actionFrontControllerSetMedia, displayFooter, displayProductAdditionalInfo, etc. Instrumentation simple : un microtime(true) au début/fin de vos hooks + logs conditionnés à _PS_MODE_DEV_. Vous verrez vite les anti-patterns : requêtes SQL non indexées, appels HTTP tiers, ou recalculs identiques sur chaque requête.
Mini-tableau d’interprétation (utile pour prioriser) :
| Symptôme | Signal courant | Action immédiate |
|---|---|---|
| Hook “vide” | aucun module greffé / hook absent | vérifier ps_hook + Positions BO |
| Hook “aléatoire” | multiboutique / exceptions | contrôler id_shop + exceptions |
| Hook “lent” | SQL répétitif / HTTP tiers | ajouter cache + profiler SQL |
| Hook “cassé” en prod seulement | cache/CCC/CDN + ordre modules | purger caches + stabiliser assets |
Pour aller plus loin sur l’analyse SQL, le slow query log et les index, voir : PrestaShop MySQL : analyser slow query log et optimiser index.
Et si vous cherchez une approche plus systématique (profiling PHP, corrélation hook ↔ requêtes, reproduction), l’article suivant complète bien cette section : méthode de diagnostic des lenteurs côté code et SQL.
Sécurité et qualité : hooks = surface d’attaque + point de régression
Les hooks d’affichage sont un point de sortie HTML, donc un point de risque XSS si vous injectez du contenu utilisateur (noms, champs personnalisés, paramètres GET/POST, données ERP/PIM). En PrestaShop, il est facile de “faire passer” une valeur dans Smarty puis de l’afficher sans échappement selon le thème. Si vous développez un module, traitez systématiquement l’encodage selon le contexte (HTML, attribut, JS, URL) et imposez des conventions côté templates. Pour une checklist de durcissement, voir : XSS : checklist de durcissement PrestaShop, CSP et encodage contexte.
Deux points « qualité » spécifiques aux hooks dynamiques (souvent oubliés) :
- Compatibilité thème : si votre module “suppose” que
displayNav2existe, vous devez le documenter (ou le créer à l’installation), sinon une migration de thème casse le rendu sans erreur explicite. - Stabilité des paramètres : sur certains hooks,
$paramsvarie selon le contrôleur et la version. Évitez de coder “au hasard” : validez la présence des clés, et dégradez proprement (return vide) si le contexte n’est pas celui attendu.
Les regex elles-mêmes peuvent devenir une vulnérabilité de type ReDoS (Regular Expression Denial of Service) si vous matchez des entrées non fiables avec des patterns à backtracking catastrophique. Dans un hook appelé sur chaque page, une regex mal conçue peut suffire à faire monter le CPU et à provoquer du 502 côté PHP-FPM. Gardez des patterns linéaires, ancrés, et évitez les quantificateurs ambiguës sur .* combinés à des alternatives.
Enfin, la qualité : les hooks dynamiques créent des régressions silencieuses quand vous migrez un thème ou que vous renommez des emplacements. Si un hook change (ex. passage Smarty → Twig, refactor thème), votre module peut “fonctionner” tout en ne rendant plus rien. La réponse sérieuse est l’automatisation : tests (au moins smoke), lint, et CI/CD avec contrôle de dépendances.
Dans un contexte e-commerce européen (et particulièrement français), où les modules “tiers” (consentement, trackers, paiement, livraison) sont nombreux, la discipline CI/CD réduit aussi les risques de régression fonctionnelle (ex. checkout) autant que les risques de sécurité. Sur la chaîne sécurisation + livraison (SBOM, WAF, pipelines), la référence interne la plus directement actionnable est : PrestaShop : sécuriser APIs et modules avec SBOM, WAF et CI/CD.




