Où se brancher dans PrestaShop, et quand préférer un override
Référence développeur pour les hooks et overrides PrestaShop : hooks d'affichage, hooks d'action, hooks personnalisés, système d'override et migration.
Ce que font réellement les hooks (et là où la plupart des gens se bloquent)
Chaque module que nous livrons chez mypresta.rocks se branche sur PrestaShop par l'intermédiaire des hooks, et après en avoir exploité 140+ en production, nous pouvons vous affirmer que la plupart des bugs de hooks que nous avons corrigés depuis 2013 remontent toujours à la même poignée d'erreurs : enregistré sur le mauvais hook, enregistré nulle part, déclenché deux fois, ou déclenché là où le développeur supposait un override.
Cette page est la référence que nous aurions aimé avoir à nos débuts. C'est aussi la page que nous envoyons aux clients lorsqu'ils nous demandent « pourquoi ce module a-t-il besoin d'une mise à jour pour PrestaShop 9 ? », car la réponse est presque toujours « le hook qu'il utilisait a changé ».
Deux types de hooks, et la seule règle qui compte
PrestaShop dispose de hooks d'affichage (ils demandent à votre module du HTML à rendre) et de hooks d'action (ils signalent à votre module qu'un événement s'est produit (commande passée, panier enregistré, client inscrit) et ignorent ce que vous retournez).
La règle qui piège tout le monde : la valeur de retour compte pour les hooks d'affichage, mais pas pour les hooks d'action. Si vous faites return $this->display(...) depuis un hook d'action, rien ne casse. Si vous oubliez le return dans un hook d'affichage, votre module ne rend rien et vous perdrez une heure à vous demander pourquoi.
Comment PrestaShop appelle réellement votre code
Lorsque le cœur atteint un point de hook, Hook::exec() interroge ps_hook_module, trie les modules actifs par position, appelle la méthode hookXxx() de chaque module avec un tableau $params, et (pour les hooks d'affichage) concatène le HTML retourné.
C'est tout. Aucune magie. Si votre hook ne se déclenche pas, c'est presque toujours à cause d'une ligne manquante dans ps_hook_module, et non d'un problème dans votre méthode.
Les quatre patterns que la plupart des modules emploient
Avant de plonger dans la documentation, demandez-vous laquelle de ces quatre tâches vous accomplissez. Choisissez le mauvais pattern et vous lutterez contre le framework pour le reste de la vie du module.
| Vous devez… | Tournez-vous vers… | Ne vous embêtez pas avec… |
|---|---|---|
| Rendre du HTML dans un thème ou une page d'administration | Un hook display* retournant un template Smarty | Faire un echo à l'intérieur de displayHeader |
| Réagir à un changement d'état (commande passée, panier enregistré, produit mis à jour) | Un hook action* | Un override du contrôleur |
| Ajouter du CSS ou du JS au front-office | actionFrontControllerSetMedia + registerStylesheet / registerJavascript | Des balises <link> brutes via displayHeader, cela détruit le CCC et casse la gestion des versions des assets |
| Modifier un calcul du cœur dépourvu de hook | Un décorateur de service Symfony (PS 8+) ou, en dernier recours, un override | Modifier les fichiers du cœur |
Les hooks que vous utiliserez vraiment
PrestaShop est livré avec des centaines de hooks. Nous nous enregistrons sur une douzaine d'entre eux environ sur l'ensemble de notre catalogue de modules. Ce sont ceux qui valent la peine d'être mémorisés.
Front-office : affichage
| Hook | Se déclenche | À utiliser pour |
|---|---|---|
displayProductAdditionalInfo | Sous le bouton Ajouter au panier | Estimations de livraison, guides des tailles, contenu additionnel de la page produit : l'emplacement le plus précieux d'une page produit |
displayShoppingCart | Page récapitulative du panier | Ventes croisées, calculateurs de frais de port, messages d'urgence |
displayOrderConfirmation | Page de remerciement après le tunnel de commande | Suivi des conversions, ventes incitatives post-achat, invitations au parrainage |
displayCustomerAccount | Page « Mon compte » | Sections personnalisées, listes de souhaits, points de fidélité, demandes de RMA |
displayBanner | Haut de page, au-dessus de l'en-tête | Bandeaux de promotion, barres de livraison gratuite, mentions RGPD |
displayHome | Zone de contenu de la page d'accueil | Collections mises en avant, modules à la une, blocs HTML réutilisables |
Back-office : affichage
| Hook | Se déclenche | À utiliser pour |
|---|---|---|
displayAdminOrder | Page de détail d'une commande | Panneaux personnalisés, intégrations de transport, statut de synchronisation ERP, signaux de fraude |
displayAdminProductsExtra | Nouvel onglet dans l'éditeur de produit | Champs produit personnalisés, données tierces |
displayBackOfficeHeader | À l'intérieur du <head> de l'administration | CSS/JS d'administration, widgets de tableau de bord, notifications de mise à jour |
Hooks d'action
| Hook | Se déclenche | À utiliser pour |
|---|---|---|
actionValidateOrder | Commande validée avec succès | Confirmation de paiement, décrément du stock, workflow post-paiement, le hook d'action le plus important |
actionOrderStatusUpdate | Changement de statut de commande | Notifications transporteur, synchronisation ERP, e-mails clients |
actionCartSave | Panier créé ou mis à jour | Déclencheurs de paniers abandonnés, réservation de stock, analytics |
actionProductUpdate | Produit enregistré dans l'administration | Synchronisation de catalogue externe, réindexation de la recherche, régénération d'images |
actionCustomerAccountAdd | Inscription d'un nouveau client | E-mail de bienvenue, synchronisation CRM, segmentation |
actionFrontControllerSetMedia | Chargement des assets par le contrôleur front | Enregistrer le CSS/JS de la bonne façon |
Le hook des assets : utilisez celui-ci, je vous prie, pas displayHeader
Depuis PrestaShop 1.7, le seul endroit correct pour ajouter du CSS ou du JS au front-office est actionFrontControllerSetMedia :
public function hookActionFrontControllerSetMedia($params)
{
$this->context->controller->registerStylesheet(
'my-module-style',
'modules/' . $this->name . '/views/css/front.css',
['media' => 'all', 'priority' => 150]
);
$this->context->controller->registerJavascript(
'my-module-script',
'modules/' . $this->name . '/views/js/front.js',
['position' => 'bottom', 'priority' => 150]
);
}
Les modules qui injectent encore des balises <link> et <script> brutes via displayHeader cassent le Combine-Compress-Cache (CCC), contournent la chaîne de requête anti-cache et ne peuvent pas être différés. Chaque fois que nous auditons une boutique lente, au moins deux modules s'y adonnent. N'en faites pas partie.
Câbler un hook dans votre module
Enregistrer à l'installation
public function install()
{
return parent::install()
&& $this->registerHook('displayProductAdditionalInfo')
&& $this->registerHook('actionCartSave')
&& $this->registerHook('actionFrontControllerSetMedia');
}
Chaque appel à registerHook() insère une ligne dans ps_hook_module. Oubliez l'enregistrement, et votre méthode de hook pourrait tout aussi bien ne pas exister, PrestaShop ne l'appellera jamais. C'est la cause la plus fréquente du « mon module ne fonctionne pas » que nous voyons en support.
Un autre piège : si vous ajoutez un nouveau hook dans la version 1.2 de votre module après avoir livré la 1.0, les installations existantes des clients ne reçoivent pas cet enregistrement. Il vous faut soit un script de mise à jour (upgrade/install-1.2.0.php) appelant registerHook(), soit des instructions pour réinitialiser le module. Indiquez-le dans les notes de version, sinon vous récolterez des tickets.
Implémenter la méthode
Le nom de la méthode est littéralement hook + le nom du hook, première lettre en majuscule :
public function hookDisplayProductAdditionalInfo($params)
{
$product = $params['product'];
$this->context->smarty->assign([
'delivery_estimate' => $this->calculateDeliveryEstimate($product),
]);
return $this->display(__FILE__, 'views/templates/hook/product-info.tpl');
}
// Hooks d'action : effectuent le travail, la valeur de retour est ignorée
public function hookActionCartSave($params)
{
if (!isset($params['cart'])) {
return;
}
$this->logCartActivity($params['cart']->id);
}
Ce qui se trouve dans $params
Cela dépend du hook. Les cinq que vous rencontrerez le plus sont $params['cart'], $params['order'], $params['product'], $params['customer'] et $params['cookie']. En cas de doute, journalisez-le :
file_put_contents(
_PS_ROOT_DIR_ . '/var/logs/hook_debug.log',
date('c') . "\n" . print_r(array_keys($params), true) . "\n",
FILE_APPEND
);
Retirez la journalisation avant la mise en production. Il nous est arrivé plus d'une fois de livrer des modules avec du code de débogage oublié, cela remplit var/logs/ sur les boutiques chargées et grignote l'espace disque.
Trouver un hook lorsque vous ignorez lequel utiliser
La page Positions dans le back-office de PrestaShop 8.
Cette page indique quels modules sont enregistrés sur quels hooks, et dans quel ordre. Si un module ne se déclenche pas, c'est le premier endroit à vérifier.
Le mode debug vous montre la page
Paramètres avancés → Performances → Mode debug → Oui. Rechargez la page. PrestaShop annote chaque point d'insertion de hook d'affichage avec son nom. Sous PS 8+, la barre d'outils Symfony liste également chaque hook déclenché lors de la requête.
Important : n'activez jamais le mode debug en production. Il expose les traces d'erreur, ralentit le site et, sous PS 9, révélera aussi les identifiants de base de données dans les pages d'erreur.
La base de données sait
-- Tous les hooks dont le nom contient « product »
SELECT name, title FROM ps_hook WHERE name LIKE '%product%' ORDER BY name;
-- Quels modules sont sur un hook donné, dans l'ordre d'exécution
SELECT m.name, 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 h.name = 'displayProductAdditionalInfo'
ORDER BY hm.position;
Faire un grep sur les sources
# Hooks d'affichage dans les templates du thème
grep -rn "{hook " themes/your-theme/templates/
# Hooks d'action dans le PHP du cœur
grep -rn "Hook::exec" classes/ controllers/ src/
Pour PS 8.x et 9.x, faites aussi un grep sur src/, les contrôleurs Symfony déclenchent eux aussi des hooks, et ils sont faciles à manquer si vous ne regardez que le répertoire hérité controllers/.
Créer votre propre hook
Ne le faites que si votre module est lui-même un point d'extension, c'est-à-dire si vous voulez que d'autres modules se branchent au vôtre. Ne créez pas de hooks juste pour organiser votre propre code ; c'est à cela que servent les méthodes.
// N'importe où dans votre module
$hookResult = Hook::exec('actionMyModuleBeforeProcess', [
'order_id' => $orderId,
'custom_data' => $myData,
]);
// Pour les hooks d'affichage, passez null + true pour obtenir le HTML par module
$extraHtml = Hook::exec('displayMyModuleExtraContent', [
'product' => $product,
], null, true);
Pour rendre votre hook visible dans Apparence → Positions, enregistrez-le une fois :
$hook = new Hook();
$hook->name = 'displayMyModuleExtraContent';
$hook->title = 'My Module : Extra Content Area';
$hook->add();
Le système d'overrides : hérité mais pas mort
Les overrides vous permettent de remplacer des méthodes du cœur sans modifier les fichiers du cœur. C'était la façon de personnaliser PrestaShop à l'époque des versions 1.5/1.6, avant que les hooks ne couvrent la plupart des cas d'usage. Ils fonctionnent encore (et nous les utilisons toujours, avec parcimonie) mais ils présentent de vrais problèmes que vous devez comprendre avant d'y recourir.
À quoi ressemble un override
// override/classes/Cart.php
class Cart extends CartCore
{
public function getOrderTotal($with_taxes = true, $type = Cart::BOTH,
$products = null, $id_carrier = null, $use_cache = false)
{
$total = parent::getOrderTotal($with_taxes, $type, $products, $id_carrier, $use_cache);
if ($this->hasSpecialProducts()) {
$total += $this->calculateSurcharge();
}
return $total;
}
}
Le répertoire des overrides reflète celui du cœur : override/classes/, override/controllers/front/, override/controllers/admin/. L'autoloader de PrestaShop regarde ici en premier, de sorte que votre Cart masque CartCore.
Pourquoi nous évitons les overrides quand nous le pouvons
- Ils entrent en conflit. Deux modules ne peuvent pas surcharger la même méthode. Le second installé échoue purement et simplement. C'est la raison nº 1 du « deux modules que je veux ne fonctionnent pas ensemble ».
- Ils cassent à la mise à jour. Les signatures de méthodes changent d'une version de PrestaShop à l'autre. Un override écrit pour la 1.7.7 peut planter sous la 1.7.8 avec une erreur fatale et mettre toute la boutique hors service.
- Ils sont invisibles. Le back-office ne donne aucun indice de l'existence d'un override. Vous déboguez un problème sans savoir que la moitié de votre classe
Cartn'est pas celle que vous croyez. - L'index des classes peut se corrompre.
cache/class_index.phpmet en cache qui surcharge quoi. Lorsqu'il se désynchronise (généralement après une installation de module ratée), la boutique affiche un écran blanc jusqu'à ce que vous supprimiez le fichier.
L'équipe du cœur de PrestaShop demande aux gens d'arrêter d'écrire des overrides depuis 2017. Ils ont raison. N'en utilisez un que lorsqu'il n'existe véritablement aucun hook, aucun service à décorer et aucun événement à écouter.
Alternatives modernes (PS 8+)
Pour PS 8 et 9, trois patterns remplacent la plupart des overrides :
Les décorateurs de service Symfony : enveloppent un service du cœur avec votre propre logique. Plusieurs modules peuvent décorer le même service sans conflit :
# modules/mymodule/config/services.yml
services:
mymodule.decorated_calculator:
class: MyModule\Service\CalculatorDecorator
decorates: 'prestashop.core.cart.calculator'
arguments:
- '@mymodule.decorated_calculator.inner'
Les écouteurs d'événements Doctrine : réagissent à la persistance des entités sans dériver ObjectModel :
# modules/mymodule/config/services.yml
services:
mymodule.product_listener:
class: MyModule\EventListener\ProductListener
tags:
- { name: doctrine.event_listener, event: postUpdate }
Les gestionnaires de commandes/requêtes CQRS : pour les opérations du back-office, décorez le bus de commandes. La courbe d'apprentissage la plus raide mais la séparation la plus propre ; c'est ce qu'utilise en interne le nouveau code du cœur de PrestaShop.
Quand deux modules se télescopent
Vous verrez ceci dans var/logs/ :
The method Cart::getOrderTotal is already overridden by module "othermodule".
Le seul véritable correctif est la fusion manuelle : ouvrez les deux fichiers d'override, combinez la logique en un seul, puis installez le second module. Après modification, supprimez l'index des classes :
rm var/cache/prod/class_index.php
rm var/cache/dev/class_index.php
C'est fragile. Lorsque l'un ou l'autre module se met à jour, votre fusion peut se casser. Documentez ce que vous avez fusionné et conservez les fichiers d'origine quelque part. Et si un module livre un override là où un hook ferait l'affaire, ouvrez je vous prie un ticket de support auprès du développeur, la plupart d'entre nous passerons aux hooks si on nous le demande.
Positions, ordonnancement et transplantations
Lorsque plusieurs modules s'enregistrent sur le même hook d'affichage, la position détermine l'ordre de rendu. Le marchand peut les réorganiser par glisser-déposer dans Apparence → Positions, en décrocher ceux dont il ne veut pas, ou transplanter un module d'un hook vers un autre.
La transplantation fonctionne dans les cas simples. Elle ne fonctionne pas toujours, et les modules qui présupposent un hook précis (parce qu'ils lisent le contexte du contrôleur, attendent certains $params ou utilisent des variables Smarty propres au hook) s'afficheront mal ou pas du tout. Si le marchand se plaint que « le module a disparu quand je l'ai déplacé », c'est pour cette raison.
Vous pouvez également définir la position par programmation à l'installation :
// Dans install() : déplacer en position 1
$this->updatePosition($this->getHookId('displayHome'), false, 1);
Déboguer lorsqu'un hook ne se déclenche pas
Parcourez ces points dans l'ordre. La cause se trouve presque toujours dans les deux premiers.
-- 1. Le module est-il enregistré sur le hook ?
SELECT m.name, 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 h.name = 'displayProductAdditionalInfo'
AND m.name = 'your_module_name';
-- 2. Le module est-il actif et installé ?
SELECT name, active FROM ps_module WHERE name = 'your_module_name';
Si les deux lignes existent et que la méthode ne se déclenche toujours pas, vérifiez :
| Symptôme | Cause la plus probable | Correctif |
|---|---|---|
| La méthode est appelée mais ne retourne rien | Chemin du template erroné, ou return manquant sur un hook d'affichage | Activez le mode debug, vérifiez le chemin du template, confirmez que vous avez retourné le rendu |
| Se déclenche sur chaque page alors que vous ne voulez que les pages produit | Aucune vérification du contrôleur | if (!($this->context->controller instanceof ProductController)) return ''; |
| Se déclenche deux fois sur la même page | Enregistré deux fois dans ps_hook_module | DELETE la ligne en double, ou réinitialisez le module |
$params['product'] est un tableau et non un objet | PS 1.7+ utilise des tableaux de présentation dans le front-office | Utilisez $params['product']['id_product'], ou appelez new Product($id) si vous avez besoin de l'objet complet |
| Fonctionne en dev, casse en prod | Templates Smarty compilés en cache | Forcer la compilation + vider le cache dans Paramètres avancés → Performances |
Profilage des hooks
Activez le profilage pour voir le temps par hook :
// PS 1.7 : defines.inc.php
define('_PS_DEBUG_PROFILING_', true);
// PS 8+ : .env.local
APP_DEBUG=1
APP_ENV=dev
Ce qui a changé dans PrestaShop 9
Si vous migrez un module, c'est la partie à lire attentivement.
Nouveaux hooks
PS 9 ajoute des hooks là où il fallait auparavant des overrides : extensions du formulaire produit en administration, application des règles de prix panier, opérations sur les ressources de l'API et envoi d'e-mails. Chaque override que nous avons supprimé lors de notre migration vers PS 9 a été remplacé par l'un d'eux.
Hooks dépréciés
Les hooks liés aux contrôleurs d'administration hérités sont dépréciés à mesure que ces contrôleurs migrent vers Symfony. La liste des dépréciations se trouve dans _PS_DEPRECATED_HOOKS_, recherchez-la dans le fichier de constantes du cœur. Votre module continuera de fonctionner, mais chaque chargement de page journalisera un avertissement de dépréciation, et le hook sera supprimé dans une future version mineure.
Les overrides deviennent moins utiles, sans disparaître
Les overrides fonctionnent encore pour les classes ObjectModel héritées. Ils ne fonctionnent pas pour les services Symfony ni les nouveaux contrôleurs d'administration. Il n'y a rien à dériver. À mesure qu'une plus grande partie du back-office migre vers Symfony, la couverture des overrides se réduit. Nous traitons chaque nouvel override que nous écrivons comme une dette technique assortie d'une date de retrait connue.
Aide-mémoire
// Enregistrer un ou plusieurs hooks
$this->registerHook(['displayHeader', 'actionCartSave']);
// Ce hook existe-t-il ?
$hookId = Hook::getIdByName('displayMyCustomHook');
// Quels modules sont sur ce hook ?
$modules = Hook::getHookModuleExecList('displayHeader');
// Déclencher un hook personnalisé
$output = Hook::exec('displayMyHook', ['key' => 'value']);
// Rendre uniquement sur les pages produit
public function hookDisplayHeader($params) {
if (!($this->context->controller instanceof ProductController)) {
return '';
}
return $this->display(__FILE__, 'views/templates/hook/header.tpl');
}
-- Tous les hooks sur lesquels un module donné est enregistré
SELECT h.name, 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;
-- Hooks sans aucun module enregistré (hooks potentiellement abandonnés)
SELECT h.name FROM ps_hook h
LEFT JOIN ps_hook_module hm ON h.id_hook = hm.id_hook
WHERE hm.id_hook IS NULL
ORDER BY h.name;
Pour aller plus loin
- Comment migrer vers PrestaShop 9 : guide complet de mise à niveau : quels hooks ont été ajoutés et dépréciés dans PS 9
- Thèmes enfants PrestaShop : guide de personnalisation Classic & Hummingbird : pour les modifications au niveau des templates, préférez un thème enfant à un override
- Outils essentiels pour le développement PrestaShop : les outils de débogage que nous utilisons au quotidien
- Maîtriser les hooks PrestaShop : une référence développeur pour 1.7, 8.x et 9.x : article compagnon plus long avec davantage d'exemples
- Checkout Revolution et Performance Revolution : deux de nos modules qui s'appuient fortement sur les hooks ; utiles comme références de production