Revu avec PrestaShop 8.x et 9.x. Tous les noms de hooks ci-dessous correspondent à de vrais hooks PrestaShop, et les modèles d’assets et de templates présentés sont à jour de la 1.7 à la 9 (cœur Symfony 6.4).
Nous publions 159 modules PrestaShop sur mypresta.rocks, et le ticket de support le plus fréquent que nous recevons d’autres développeurs qui lisent notre code ressemble presque toujours à ceci : « mon hook ne se déclenche pas ». Le nom de la méthode peut être incorrect, comme l’explique la liste de vérifications ci-dessous. C’est presque toujours l’une de ces quatre causes : le module est enregistré sur le mauvais hook, il n’a jamais été enregistré, il a été enregistré deux fois, ou il est bien enregistré mais appelé dans un contexte que le développeur n’avait pas prévu. Voici le guide de terrain que nous avons fini par écrire pour nous-mêmes au fil des années.
Pour le dictionnaire complet des hooks, les compromis liés aux overrides et les notes de migration PS 8/9, la page référence PrestaShop Hooks & Overrides va beaucoup plus loin. Cet article en est le compagnon pratique : moins de tableaux, plus de code réellement utilisé en production, et les pièges précis dans lesquels nous sommes nous-mêmes tombés.
Partez du besoin, pas du nom du hook
Choisissez le hook en remontant depuis le comportement dont vous avez besoin. Si votre module a seulement besoin de CSS sur les pages produit, n’injectez pas de balisage dans displayHeader. Si vous devez réagir après la création d’une commande, n’accrochez pas votre logique à un hook d’affichage sur la page de remerciement (nous reviendrons sur la raison). Si vous reconstruisez le tunnel de commande, attendez-vous à vous enregistrer sur plusieurs hooks et acceptez qu’un seul hook ne puisse pas porter tout le module.
| Tâche du module | Hook préférable | Pourquoi | Erreur fréquente |
|---|---|---|---|
| Charger le CSS ou le JavaScript du module | actionFrontControllerSetMedia |
Enregistre les assets via le pipeline de PrestaShop, une fois le contexte du contrôleur déjà résolu. | Afficher directement des balises <script> depuis displayHeader, cela court-circuite le CCC et le cache-busting. |
| Ajouter un bloc de réassurance produit | displayProductAdditionalInfo |
S’affiche juste à côté du bouton d’achat et vous transmet le produit dans $params. |
S’accrocher à une position globale du pied de page et recalculer l’ID produit à la main. |
| Réagir après une modification du panier | actionCartSave |
Se déclenche après chaque mise à jour du panier, sans aucun rendu visible. | Lancer des appels d’API distants à chaque sauvegarde, puis s’étonner que la page panier soit lente. |
| Mettre en file une synchro ERP / CRM après achat | actionValidateOrder |
Se déclenche lorsque la commande est réellement créée, avec la commande, le panier, le client et la devise dans le contexte ; prévoyez une protection contre les appels en double. | Synchroniser depuis la page de remerciement, qui se déclenche à nouveau lors d’un rafraîchissement et après les retours de paiement. |
Vérifiez le véritable écran Positions
Avant de déboguer le code, regardez le Back Office. La page Positions vous indique quels modules sont actuellement attachés à chaque hook, dans quel ordre ils se déclenchent, et si le hook est actif pour la boutique. Nous l’utilisons tous les jours : une méthode de hook parfaite ne s’exécutera jamais si le module n’a pas été enregistré pendant l’installation, s’il a été greffé sur le mauvais hook, ou si un marchand l’a décroché pendant un changement de thème.
Véritable écran Positions de PrestaShop 9
Il provient de notre boutique ps9-dev, pas d’une maquette. Le champ de filtre en haut est le moyen le plus rapide de confirmer qu’un hook existe et de voir qui d’autre y est attaché.
Pour un travail sur la page produit, filtrez displayProductAdditionalInfo. Pour le chargement des assets, actionFrontControllerSetMedia. Pour les intégrations liées aux commandes, actionValidateOrder. Si le nom de votre module n’apparaît pas dans cette liste, aucune réécriture de méthode ne le corrigera.

N’enregistrez que les hooks que vous utilisez
L’enregistrement des hooks se fait dans install() pour les nouvelles installations et dans les scripts de mise à niveau lors de l’ajout de hooks aux installations existantes. Gardez une liste explicite. Un module qui s’enregistre sur vingt hooks mais n’implémente que trois méthodes ajoute une ligne dans ps_hook_module pour chaque hook vide, et sur les pages chargées, PrestaShop parcourt quand même cette liste avant de décider qu’il n’y a rien à faire. Nous avons audité des modules enregistrés sur plus de 40 hooks « au cas où ». Ne faites pas partie de ceux-là.
<?php
public function install()
{
return parent::install()
&& $this->registerHook('actionFrontControllerSetMedia')
&& $this->registerHook('displayProductAdditionalInfo')
&& $this->registerHook('actionValidateOrder');
}
Si vous ajoutez un hook en v1.2 alors que le module circule déjà en v1.0, les boutiques existantes ne relancent jamais install(). Il vous faut un script de mise à niveau dans upgrade/install-1.2.0.php :
<?php
function upgrade_module_1_2_0($module)
{
return $module->registerHook('actionCartSave');
}
Il nous est déjà arrivé de publier des versions sans le fichier de mise à niveau. La file de tickets le rend vite évident.
Exemple : charger les assets à la manière PrestaShop
actionFrontControllerSetMedia est le seul endroit correct pour le CSS ou le JS du front-office depuis la 1.7. Il s’exécute après que le contrôleur a déterminé quelle page est rendue, ce qui permet de cibler proprement un contrôleur précis. Les modules qui affichent encore des balises <link> depuis displayHeader contournent entièrement le CCC, ne peuvent pas être différés et ne reçoivent pas la chaîne de cache-busting. Chaque audit de boutique lente que nous avons réalisé ces trois dernières années en a trouvé au moins deux.
<?php
public function hookActionFrontControllerSetMedia(array $params): void
{
$controller = $this->context->controller;
if (!isset($controller->php_self) || $controller->php_self !== 'product') {
return;
}
$controller->registerStylesheet(
'my-module-product',
'modules/' . $this->name . '/views/css/product.css',
['media' => 'all', 'priority' => 150]
);
$controller->registerJavascript(
'my-module-product',
'modules/' . $this->name . '/views/js/product.js',
['position' => 'bottom', 'priority' => 150]
);
}
Le garde-fou php_self compte. Sans lui, chaque page catégorie, page CMS, page de recherche et étape du tunnel de commande paie le coût d’assets propres aux pages produit : requêtes HTTP supplémentaires, plus d’octets à travers PurgeCSS, First Contentful Paint plus lent. Lorsque nous avons construit Performance Revolution, le problème le plus courant que nous avons vu sur les boutiques lentes était exactement ce schéma : un module qui enregistre des assets globaux alors qu’il n’en a besoin que sur un seul contrôleur.
Exemple : rendre un bloc sur la page produit
Les hooks d’affichage renvoient du balisage. Le schéma est simple : extraire ce dont vous avez besoin depuis $params, l’assigner à Smarty, puis rendre un template que le thème peut surcharger.
<?php
public function hookDisplayProductAdditionalInfo(array $params): string
{
$productId = (int) ($params['product']['id_product'] ?? Tools::getValue('id_product'));
if ($productId <= 0) {
return '';
}
$this->context->smarty->assign([
'mpr_delivery_label' => $this->getCachedDeliveryLabel($productId),
'mpr_support_url' => $this->context->link->getCMSLink(3),
]);
return $this->fetch('module:' . $this->name . '/views/templates/hook/product-trust.tpl');
}
Deux détails issus de la production. D’abord, en 1.7+, $params['product'] est un tableau de présentation, pas un objet Product : si vous l’oubliez et appelez $params['product']->reference, vous obtenez une erreur fatale en PHP 8.1+. Ensuite, ce fetch() avec le préfixe module: est ce qui permet à un thème enfant de surcharger le template dans themes/your-theme/modules/your-module/views/templates/hook/product-trust.tpl. Si vous utilisez plutôt $this->display(__FILE__, ...), la surcharge par le thème ne fonctionne pas silencieusement, et vous recevrez un e-mail agacé de la personne qui maintient le thème.
C’est le schéma que nous utilisons pour les badges de réassurance, les messages de livraison, les notes de garantie, les sélecteurs de variantes personnalisés et le widget de financement que nos boutiques de démonstration Checkout Revolution affichent sur chaque produit. Même structure, à chaque fois.
Exemple : mettre le travail commande en file, sans bloquer le tunnel
Les hooks de commande donnent l’impression d’être une invitation gratuite à faire tout ce qui doit arriver « lorsqu’une commande est passée ». Ne le faites pas. actionValidateOrder se déclenche à l’intérieur de la transaction du tunnel de commande : le client est encore sur la page et attend la redirection vers la page de remerciement. Si vous appelez un webhook ERP de façon synchrone et que l’ERP met quatre secondes à répondre, votre client attend quatre secondes. Si l’ERP est hors service, la requête de commande peut expirer complètement.
<?php
public function hookActionValidateOrder(array $params): void
{
/** @var Order $order */
$order = $params['order'];
if ($this->syncAlreadyQueued((int) $order->id)) {
return;
}
$this->queueOrderSync([
'id_order' => (int) $order->id,
'id_customer' => (int) $order->id_customer,
'total_paid' => (float) $order->total_paid_tax_incl,
'created_at' => date('Y-m-d H:i:s'),
]);
}
Le contrôle syncAlreadyQueued() mérite sa place. Nous avons eu une boutique cliente sur une passerelle de paiement instable où actionValidateOrder se déclenchait deux fois sur environ 0,3 % des commandes : une première fois sur le paiement réel, une seconde sur une nouvelle tentative que la passerelle traitait avant d’expirer. L’intégration ERP créait joyeusement deux bons de préparation d’entrepôt par commande pendant des semaines avant que quelqu’un ne s’en rende compte. Sur ce hook, l’idempotence n’est pas de la paranoïa : c’est le prix à payer quand on travaille avec de vrais prestataires de paiement.
Pourquoi actionValidateOrder et pas displayOrderConfirmation ? Parce que la page de confirmation se déclenche à chaque fois que le client la rafraîchit ou revient d’un prestataire de paiement, et vous lancerez votre synchronisation plusieurs fois pour la même commande. actionValidateOrder se déclenche quand la ligne de commande est réellement créée, mais les appels en double décrits ci-dessus exigent un traitement idempotent.
Déboguer l’enregistrement des hooks avec SQL
Si une méthode ne s’exécute pas, prouvez que le hook est enregistré avant de modifier quoi que ce soit d’autre. La page Positions est le contrôle visuel ; SQL est plus rapide lorsque vous avez besoin de chiffres.
SELECT h.name, COUNT(hm.id_module) AS modules
FROM ps_hook h
LEFT JOIN ps_hook_module hm ON hm.id_hook = h.id_hook
WHERE h.name IN (
'actionFrontControllerSetMedia',
'displayProductAdditionalInfo',
'actionCartSave',
'actionValidateOrder',
'displayAdminOrder'
)
GROUP BY h.name
ORDER BY h.name;
Sur notre boutique mypresta.rocks en production, les hooks courants peuvent accumuler une longue file de modules enregistrés. Chaque gestionnaire attaché à actionFrontControllerSetMedia s’exécute en séquence lors du chargement des pages du front-office ; chacun doit donc être rapide. Un gestionnaire lent multiplié par une file de hooks chargée se transforme en TTFB visible avant même d’avoir atteint le template.
L’autre requête que nous lançons constamment est : « le module est-il attaché, oui ou non ? »
SELECT m.name, h.name AS hook, hm.position
FROM ps_hook_module hm
JOIN ps_hook h ON h.id_hook = hm.id_hook
JOIN ps_module m ON m.id_module = hm.id_module
WHERE m.name = 'your_module_name'
ORDER BY h.name;
Si la ligne n’est pas là, votre install() ne s’est pas exécuté, ou il s’est exécuté puis a échoué silencieusement après l’installation parente. C’est le bug. Pas la méthode.
Le hook personnalisé que nous n’avons pas pu éviter
Nous essayons de ne pas créer de hooks personnalisés. La plupart des décisions du type « je vais ajouter un hook ici pour l’extensibilité » vieillissent mal : le hook finit par n’être utilisé par personne sauf le module d’origine, et c’est désormais une API publique que vous devez maintenir. Mais il existe une exception qui mérite d’être expliquée, parce qu’elle montre quand un hook personnalisé est le bon choix.
Dans l’un de nos plus gros modules, l’éditeur de produit en administration possède un onglet personnalisé avec ses propres champs de formulaire. D’autres modules voulaient étendre cet onglet : ajouter une ligne, injecter un réglage, afficher un badge d’état. Nous ne voulions pas que chaque module d’extension surcharge le même fichier de template (conflits d’override, voir la référence de la base de connaissances). Nous avons donc ajouté un seul hook d’affichage personnalisé, déclenché depuis le template de l’onglet :
// In the module that owns the tab
$extraRows = Hook::exec(
'displayMprProductTabExtra',
['id_product' => (int) $idProduct, 'tab' => 'logistics'],
null,
true
);
// $extraRows is an associative array keyed by module name
// Render each extension's HTML inside the tab
Et nous avons enregistré le hook une seule fois à l’installation, afin qu’il apparaisse dans Design → Positions comme n’importe quel autre :
$hook = new Hook();
$hook->name = 'displayMprProductTabExtra';
$hook->title = 'Product tab – extra content area';
$hook->add();
La règle pratique : créez un hook personnalisé uniquement lorsque votre module est lui-même un point d’extension et que d’autres modules veulent s’y brancher. Ne créez pas de hooks pour organiser votre propre code interne : les méthodes servent à cela.
Le seul override que nous livrons encore
La page de référence affirme clairement que les overrides sont hérités du passé et qu’il faut les éviter. Nous sommes d’accord. Mais nous livrons encore exactement un override dans tout notre catalogue, et cela vaut la peine d’expliquer pourquoi, car le même raisonnement s’applique ailleurs.
Il s’agit d’un override sur StockAvailable, dans un module d’entrepôt qui doit intercepter la manière dont le stock est décrémenté pour les configurations multi-entrepôts. Le comportement dont nous avons besoin dépend de l’entrepôt depuis lequel une commande donnée est préparée : il n’existe aucun hook dans le chemin de décrémentation du stock, aucun service Symfony à décorer sur PS 1.7 (que les clients concernés utilisent encore), et aucun écouteur d’événement. Le comportement souhaité est « étendre la méthode existante, pas la remplacer ». Exactement ce pour quoi les overrides sont faits.
C’est documenté en très gros dans le README du module. Il entre en conflit avec tout autre module qui surcharge StockAvailable. Nous avons accepté ce compromis parce que l’alternative (le corriger autrement) serait pire. Chaque override est une dette technique avec une date de retrait connue ; pour celui-ci, la date de retrait est « quand notre dernier client en 1.7 migrera vers PS 9 ».
Quand un hook n’est pas le bon outil
Les hooks ne sont pas une autorisation de concentrer chaque personnalisation dans une seule méthode. Si vous remplacez tout un flux de travail, construisez un vrai contrôleur ou une couche de services, et utilisez les hooks seulement comme points d’intégration. Si vous modifiez largement la structure des templates, un thème enfant est plus propre. Si vous corrigez ce qui ressemble à un bug du cœur, prouvez la cause racine avant de l’enrober de logique de hook.
| Situation | Utiliser un hook ? | Meilleure direction |
|---|---|---|
| Ajouter un petit bloc visible aux pages produit | Oui | Hook d’affichage avec prise en charge de la surcharge de template via le préfixe module:. |
| Charger un script propre à une page | Oui | actionFrontControllerSetMedia avec un garde-fou php_self. |
| Remplacer la mise en page et le comportement du tunnel de commande | En partie | Une véritable architecture de module ; les hooks l’intègrent à PrestaShop. Checkout Revolution reconstruit le tunnel de commande de bout en bout au lieu de faire semblant qu’un seul hook peut tout porter. |
| Modifier chaque détail du template produit | Parfois | Un thème enfant est généralement plus propre, voir le guide des thèmes enfants. |
| Corriger des problèmes de performance causés par de nombreux modules | Non | Profilez la pile de hooks, puis réduisez, mettez en cache ou retirez les gestionnaires lents. Voir le guide des performances. |
Les quatre vérifications qui attrapent la plupart des bugs de hooks
Lorsque nous ouvrons un ticket de support qui dit « le hook ne se déclenche pas », la réponse est l’une de ces quatre causes, à peu près dans cet ordre de fréquence :
- Le module n’a pas été enregistré sur le hook. Vérifiez
ps_hook_module. Si la ligne manque, votreinstall()n’a pas exécuté cette ligne, ou vous avez ajouté le hook dans une version ultérieure sans script de mise à niveau. - Le nom de la méthode est incorrect. Utilisez
hookActionValidateOrder, pasactionValidateOrder;HookActionValidateOrderne diffère que par la casse et fonctionne également. PHP ne tient pas compte de la casse pour les noms de méthodes, mais une faute dans le préfixe reste une faute. - Vous ne renvoyez rien depuis un hook d’affichage. Les hooks d’affichage doivent
returnle HTML ; les hooks d’action ignorent la valeur de retour. Il est facile de les intervertir par accident. - Le hook se déclenche dans un contexte que vous n’aviez pas prévu.
displayHeaderse déclenche aussi sur les requêtes AJAX.displayOrderConfirmationse déclenche à chaque rafraîchissement.actionCartSavese déclenche lorsque le panier est sauvegardé pour la session du client, même si rien n’a changé. Protégez le gestionnaire.
Parcourez ces quatre points dans l’ordre, et vous n’aurez presque jamais besoin d’ouvrir un profiler Symfony.
FAQ
Quelle est la différence entre un hook « display » et un hook « action » ?
Le nom vous donne le contrat. Un hook display* attend que vous fassiez un return de HTML, que PrestaShop affiche à cet endroit : ne rien renvoyer donne un emplacement vide, pas une erreur. Un hook action* est un événement : PrestaShop ignore votre valeur de retour et exécute simplement votre code pour ses effets de bord (mettre une synchro en file, écrire un log, enregistrer une ligne). Renvoyer du HTML depuis un hook d’action ne fait rien ; oublier de renvoyer du HTML depuis un hook d’affichage est le bug silencieux de la troisième vérification ci-dessus.
J’ai ajouté un nouveau hook dans une version ultérieure : pourquoi n’est-il pas enregistré sur les boutiques existantes ?
install() ne s’exécute qu’une seule fois, lors de la première installation du module. Les boutiques qui ont installé une version antérieure ne le relancent jamais ; une ligne registerHook() ajoutée en v1.2 leur reste donc invisible jusqu’à une réinstallation. La correction consiste à ajouter un script de mise à niveau dans le répertoire upgrade/ du module : PrestaShop l’exécute automatiquement lorsque le marchand met le module à jour, comme montré plus haut dans cet article. Livrez l’enregistrement à la fois dans install() (pour les nouvelles boutiques) et dans le script de mise à niveau (pour les boutiques existantes).
Les noms des hooks ont-ils changé dans PrestaShop 9 ?
Les hooks front-office principaux de cet article, actionFrontControllerSetMedia, displayProductAdditionalInfo, actionValidateOrder, actionCartSave, sont stables entre 1.7, 8 et 9. Ce qui a changé en 9, c’est le framework autour (Symfony 6.4) et certaines anciennes API supprimées ; un gestionnaire qui appelle un helper déprécié comme Tools::displayPrice() peut donc provoquer une erreur fatale en 9, même si le hook lui-même se déclenche toujours. Le hook va bien ; c’est le code à l’intérieur qu’il faut auditer lorsque vous passez à la 9.
Comment trouver quel hook une position du thème rend réellement ?
L’écran Positions (Design → Positions) est la carte visuelle la plus rapide : il liste chaque hook, les modules qui y sont attachés et leur ordre. Filtrez par nom de hook pour confirmer qu’un hook existe et voir qui d’autre y est présent. Lorsque vous avez besoin de chiffres plutôt que d’un balayage visuel, des comptes, ou un oui/non pour savoir si votre module est attaché, les deux requêtes SQL plus haut dans cet article, sur ps_hook et ps_hook_module, sont plus rapides.
Est-il parfois acceptable d’afficher un <script> depuis displayHeader ?
Presque jamais pour les assets du front-office. Depuis la 1.7, le CSS et le JS vont dans actionFrontControllerSetMedia via registerStylesheet() / registerJavascript(), afin de passer par le pipeline et de bénéficier du CCC, du cache-busting et du différé. Les balises brutes depuis displayHeader contournent tout cela et apparaissent dans tous nos audits de boutiques lentes. L’exception étroite concerne les données inline que le pipeline d’assets ne peut pas transporter : un bloc de données structurées ou un petit objet de configuration de bootstrap ; même dans ce cas, gardez-le minuscule.
À lire également
- référence PrestaShop Hooks & Overrides, le compagnon long format : dictionnaire complet des hooks, compromis liés aux overrides, notes de migration PS 9
- Comment migrer vers PrestaShop 9 : guide complet de mise à niveau. Ce qui a changé pour les hooks spécifiquement dans PS 9
- Guide des thèmes enfants PrestaShop, quand les surcharges de templates sont préférables à la personnalisation par hooks
- Guide de dépannage PrestaShop, prouver la cause racine avant de choisir un hook
- Guide des performances PrestaShop, profiler la pile de hooks sur une boutique lente
- Checkout Revolution et Performance Revolution, deux de nos modules qui s’appuient fortement sur les hooks ; des références de production utiles