Hooks PrestaShop : rechercher et identifier les hooks dynamiques

Guide pratique pour repérer, tracer et valider les hooks dynamiques d’ObjectModel dans PrestaShop, avec recherches, instrumentation, vérifications SQL et checklist.

Environnement de développement avec visualisation de hooks dynamiques de PrestaShop.

Table des matières :

  1. Hooks statiques vs hooks dynamiques : ce qui change réellement
  2. Les hooks dynamiques d’ObjectModel : nomenclature, paramètres, pièges
  3. Trouver les hooks dynamiques dans le code : recherche full-text + regex utiles
  4. Identifier “ce qui s’exécute vraiment” : instrumentation et traces reproductibles
  5. Vérifier côté base : ps_hook, ps_hook_module, alias et “hooks fantômes”
  6. Exploiter un hook dynamique proprement : contrat d’entrée, perf, et tests
  7. Checklist terrain : rechercher et valider un hook dynamique sans perdre une journée

Hooks statiques vs hooks dynamiques : ce qui change réellement

Dans PrestaShop (8.1.x, 9.0–9.1) le mot hook désigne un point d’extension où le cœur appelle du code de modules via Hook::exec(). La définition officielle est volontairement simple : « Hooks are a way to trigger modules at certain points in the shop » (PrestaShop DevDocs — Hooks). En pratique, côté dev, la différence qui compte n’est pas “display vs action”, mais hook nommé explicitement vs hook construit dynamiquement.

Un hook “statique” est trivial à trouver : Hook::exec('displayHeader'), {hook h='displayFooter'} dans Smarty, ou un hook BO documenté comme actionProductGridDataModifier (voir l’article interne : actionProductGridDataModifier — article interne). Un hook “dynamique”, lui, est généré par concaténation de chaînes (ou à partir d’un nom de classe), ce qui casse les approches naïves du type “je cherche actionObjectProductUpdateAfter dans le repo”. Le hook existe bien au runtime, mais son nom n’apparaît pas forcément tel quel dans le code.

Ce point est plus qu’un détail “académique” : en maintenance PrestaShop, c’est souvent la différence entre 5 minutes et une demi-journée. Exemple très courant : un import catalogue (ERP/PIM, CSV, Webservice) met à jour des produits, mais l’équipe attend un hook “métier BO” qui ne se déclenche jamais, parce que l’import passe par ObjectModel::update() sans emprunter la même couche UI.

Le résultat opérationnel est connu : vous savez qu’un événement métier se produit (un produit est mis à jour, une commande est validée), mais vous ne savez pas quel hook intercepter sans lire du core (et parfois plusieurs couches : ObjectModel, Adapter, contrôleurs legacy, contrôleurs Symfony BO). Le but ici est de rendre cette recherche reproductible, sans heuristiques magiques et sans dépendre d’un module “debug hooks” approximatif.

Deux conséquences concrètes à garder en tête dès le début :

  • “Un hook existe” ≠ “mon module est appelé” : il faut l’enregistrement (registerHook() → ps_hook_module) + l’absence d’exceptions + le bon contexte boutique.
  • Le bon niveau d’interception dépend de l’intention : si vous voulez réagir à la persistance, les hooks dynamiques d’ObjectModel sont souvent plus fiables ; si vous voulez réagir à une action d’interface (BO Symfony, grids, forms), un hook documenté (type action...Grid...Modifier) ou un événement Symfony sera parfois plus stable.

Les hooks dynamiques d’ObjectModel : nomenclature, paramètres, pièges

Le premier gros bloc de hooks dynamiques vient de classes/ObjectModel.php : lors d’un add(), update(), delete(), PrestaShop exécute des hooks générés à partir de la classe. Typiquement, le cœur construit des noms du style :

  • actionObject<ClassName>AddBefore / actionObject<ClassName>AddAfter
  • actionObject<ClassName>UpdateBefore / actionObject<ClassName>UpdateAfter
  • actionObject<ClassName>DeleteBefore / actionObject<ClassName>DeleteAfter

Concrètement, pour Product cela donne actionObjectProductUpdateAfter, pour Order : actionObjectOrderAddAfter, etc. Le point important : ces hooks ne sont pas “documentés” au même niveau que les hooks fonctionnels, mais ils sont souvent la meilleure interception disponible quand vous devez réagir à la persistance plutôt qu’à une action UI.

Pour vous donner un repère rapide, voici un mini-tableau de lecture (utile quand on débogue un “ça ne se déclenche pas”) :

Intention Hook dynamique typique Quand l’utiliser Quand l’éviter
Bloquer/valider avant écriture ...UpdateBefore / ...AddBefore Ajuster l’objet (valeur par défaut, normalisation) avant DB Si vous risquez de déclencher des effets de bord (requêtes lourdes, appels externes)
Réagir après écriture ...UpdateAfter / ...AddAfter Indexation, synchronisation, invalidation cache, logs Si vous avez besoin d’un contexte UI précis (qui a cliqué où)
Nettoyer avant suppression ...DeleteBefore Détacher des relations, vérifier des contraintes Si vous devez encore accéder à des infos déjà supprimées ailleurs
Post-suppression ...DeleteAfter Purge, suppression de fichiers, cleanup cache Attention aux références inexistantes (l’objet n’est plus en base)

Côté paramètres, ObjectModel passe en général un payload minimal, typiquement ['object' => $this] (donc une instance du modèle) et parfois d’autres informations selon la méthode (ex. id/id_object, champs modifiés, etc. selon versions). Ne partez pas du principe que $params['id_product'] existe : sur ces hooks, le contrat le plus stable est $params['object']. Le pattern robuste consiste à : (1) vérifier l’instance, (2) lire l’ID via propriété, (3) recharger si vous avez besoin d’un état complet (avec prudence sur les performances).

Deux pièges supplémentaires (souvent rencontrés en boutique réelle) :

  • Multi-boutique : une mise à jour peut être “globale” ou scindée par boutique selon l’écran BO et la configuration. Le hook actionObjectProductUpdateAfter se déclenche, mais le contenu pertinent (prix, stock, champs traduits) peut dépendre de id_shop et du contexte courant. Si votre logique doit être par boutique, récupérez explicitement l’id_shop (si disponible dans le contexte) et chargez les données en conséquence au lieu de supposer “une vérité unique”.
  • Récursivité involontaire : si, dans ...UpdateAfter, vous modifiez le même objet puis appelez update(), vous pouvez ré-entrer dans le hook et créer une boucle (ou, au minimum, doubler le volume de requêtes). La défense classique : un garde-fou (flag, stockage temporaire, ou détection de changement) + rendre le traitement idempotent.

Le piège rarement dit explicitement : ces hooks dynamiques se basent sur get_class($this) (ou une variante équivalente selon versions). Si vous créez un ObjectModel custom namespaced (ex. Vendor\Module\Model\Foo extends ObjectModel), le nom de hook généré contiendra des \ et devient ingérable (actionObjectVendor\Module\Model\FooUpdateAfter). Le core ne “normalise” pas ce nom. En clair : si vous voulez tirer parti de ces hooks, gardez les classes ObjectModel legacy non-namespaced, ou implémentez vos propres points d’extension (ou événements Symfony) dans votre module.

Astuce pragmatique : dans beaucoup d’équipes, on garde un dossier classes/ dans le module avec des classes non-namespacées uniquement pour bénéficier proprement des hooks d’ObjectModel, et on réserve les namespaces aux services Symfony/Domain.

Trouver les hooks dynamiques dans le code : recherche full-text + regex utiles

Pour identifier un hook dynamique, la méthode la plus rapide reste la lecture ciblée du core, mais avec des requêtes de recherche adaptées. Sur un checkout local (ou un clone du repo PrestaShop correspondant à la version réelle), utilisez ripgrep (rg). Si vous ne l’avez pas déjà dans votre trousse, c’est un binaire ultra-rapide (référence : ripgrep — GitHub).

# Hooks ObjectModel (concaténation)
rg "actionObject" classes/ -n

# Toute concaténation autour de Hook::exec()
rg "Hook::exec\(" -S -n
rg "Hook::exec\(.*\." -S -n  # cherche un point (concat) dans les arguments

Pour aller plus loin sans “ouvrir 40 fichiers”, utilisez des patterns un peu plus ciblés (PCRE2) :

# Hooks dynamiques ObjectModel avec variantes Before/After
rg -n -P "actionObject[A-Za-z0-9_]+(Add|Update|Delete)(Before|After)" classes/

# Cas où le nom est construit via sprintf
rg -n -P "Hook::exec\(\s*sprintf" classes/ controllers/ src/

Ensuite, cherchez les templates : sur le Front Office, les hooks d’affichage passent souvent par Smarty (.tpl) :

rg "\{hook\s+h=" themes/ modules/ -n

Les hooks dynamiques côté template existent aussi (moins fréquents), typiquement quand un thème compose un nom à partir d’une variable. Ce n’est pas une bonne pratique (ça complique support et audit), mais ça existe en production sur des thèmes sur-mesure. Si vous voyez un {hook h=$someVar} ou une concat Smarty, notez l’endroit exact : le “hook” que vous cherchez est peut-être une convention interne du thème, pas un hook core.

Quand la concaténation est en PHP (cas le plus intéressant), l’astuce consiste à remonter de l’intention métier vers la classe : “mise à jour produit” → Product extends ObjectModel → cherchez dans ObjectModel::update() les Hook::exec() autour.

Un bon complément (souvent oublié) consiste à chercher les enregistrements de hooks côté modules, parce que ça vous indique parfois directement le “bon” nom (y compris dynamique) :

# Où les modules déclarent/registrent des hooks
rg -n "registerHook\(" modules/

# Où ils implémentent les handlers hookXxx()
rg -n -P "function\s+hook[A-Za-z0-9_]+\s*\(" modules/

Enfin, un lien GitHub direct vers le fichier (pour lecture rapide, même sans IDE) : ObjectModel.php (GitHub) (adaptez la branche/tag à votre version, sinon vous comparez des comportements différents). Même remarque pour classes/Hook.php : si vous instrumentez, vous devez être sur la bonne version du fichier, sinon vous logguez “au mauvais endroit”.

Identifier “ce qui s’exécute vraiment” : instrumentation et traces reproductibles

La recherche statique ne suffit pas dès que vous avez (a) des overrides, (b) des modules qui appellent eux-mêmes Hook::exec() avec des noms custom, (c) du code conditionnel dépendant du contexte (boutique, groupe, device, pays). Dans ces cas, il faut instrumenter l’exécution.

Le point de départ propre est le débogage local : Xdebug + VS Code vous donne la stack exacte jusqu’à Hook::exec() (setup pas-à-pas dans l’article interne : Xdebug + VS Code — article interne). En pratique, pour “identifier un hook dynamique”, l’objectif n’est pas juste d’atteindre Hook::exec() : c’est de capturer :

  • le nom final du hook (après concat/sprintf),
  • le payload ($hook_args) effectivement passé,
  • le contexte (shop, controller, BO/FO),
  • et si possible quels modules répondent (et leur temps d’exécution).

Si vous avez besoin d’une liste exhaustive des hooks exécutés sur un parcours (ex. page produit + add-to-cart + paiement), un mécanisme simple est un override temporaire de classes/Hook.php (ou une surcharge si votre version l’autorise encore), qui loggue : nom de hook, module(s) appelés, durée, mémoire. Attention : c’est intrusif, peut impacter la perf et la compatibilité, et ne doit pas finir en prod. Sur PrestaShop 9, la direction générale est de réduire la dépendance aux overrides ; considérez plutôt un patch maintenu (Composer patches) ou un environnement de staging dédié.

Exemple minimaliste (staging uniquement) : journaliser le hook + temps dans un fichier.

// pseudo-code : à adapter à votre version et stratégie (override/patch)
public static function exec($hook_name, $hook_args = [], $id_module = null, $array_return = false, $check_exceptions = true, $use_push = false, $id_shop = null)
{
    $t0 = microtime(true);
    $res = parent::exec($hook_name, $hook_args, $id_module, $array_return, $check_exceptions, $use_push, $id_shop);
    $dt = (microtime(true) - $t0) * 1000;
    error_log(sprintf('[hook] %s %.2fms', $hook_name, $dt));
    return $res;
}

Pour rendre la trace exploitable, vous pouvez enrichir la ligne de log (toujours en staging) avec :

  • l’URI et/ou le contrôleur (utile pour distinguer FO/BO),
  • id_shop si présent,
  • le nombre de modules exécutés (si vous le récupérez),
  • et une corrélation par requête (un identifiant unique par hit) pour reconstituer un parcours dans les logs.

Cette instrumentation sert aussi à détecter des hooks “bruyants” (ex. un displayHeader déclenchant 30 modules) et à corréler avec des symptômes serveur (latence, CPU). Si vous voyez des spikes, recoupez avec un audit perf et du slow query log (article interne : slow query log — article interne), sinon vous allez optimiser à l’aveugle. Et si l’analyse vous montre que le temps n’est pas en SQL mais en CPU PHP, il devient pertinent de regarder OPcache, le cache applicatif, ou la volumétrie des hooks FO (ex. displayHeader) plutôt que de “micro-optimiser” un handler.

Vérifier côté base : ps_hook, ps_hook_module, alias et “hooks fantômes”

Même si un hook est exécuté par le core, un module ne sera appelé que s’il est enregistré sur ce hook (et si la boutique/exception le permet). L’état de vérité est dans la base : ps_hook, ps_hook_module, ps_hook_alias (préfixe à adapter). Une recherche “je ne comprends pas pourquoi mon hook ne part pas” commence souvent par une requête SQL simple.

Lister les modules branchés sur un hook donné :

SELECT h.name AS hook, m.name AS module, hm.position
FROM ps_hook h
JOIN ps_hook_module hm ON hm.id_hook = h.id_hook
JOIN ps_module m ON m.id_module = hm.id_module
WHERE h.name = 'actionObjectProductUpdateAfter'
ORDER BY hm.position ASC;

Lister les hooks “orphelins” (créés mais sans module), utile après des désinstallations sales :

SELECT h.id_hook, h.name
FROM ps_hook h
LEFT JOIN ps_hook_module hm ON hm.id_hook = h.id_hook
WHERE hm.id_hook IS NULL
ORDER BY h.name;

Point important : un hook dynamique comme actionObjectProductUpdateAfter peut être absent de ps_hook tant qu’aucun module n’a tenté de s’enregistrer dessus (via registerHook()). N’essayez pas de “créer” des hooks à la main en SQL : vous allez vous battre avec les shops, les positions, les alias, et surtout vous cassez la traçabilité. La création doit passer par l’install du module (voir structure et bonnes pratiques sur PrestaShop 9 : module PrestaShop 9 — bonnes pratiques).

Deux contrôles très utiles en diagnostic (notamment quand “ça marche en dev mais pas en prod”) :

1) Les exceptions de hooks : un module peut être enregistré sur un hook, mais exclu sur certains contrôleurs/pages (table ps_hook_module_exceptions sur beaucoup d’installations).

SELECT h.name AS hook, m.name AS module, hme.file_name
FROM ps_hook_module_exceptions hme
JOIN ps_hook h ON h.id_hook = hme.id_hook
JOIN ps_module m ON m.id_module = hme.id_module
WHERE h.name = 'displayHeader'
ORDER BY m.name, hme.file_name;

2) Les alias de hooks : certains hooks ont des noms historiques/compatibilité. La table ps_hook_alias peut expliquer pourquoi un module “vise” un nom et se retrouve quand même exécuté (ou l’inverse).

SELECT ha.name, ha.alias
FROM ps_hook_alias ha
WHERE ha.name LIKE '%header%' OR ha.alias LIKE '%header%'
ORDER BY ha.name, ha.alias;

Ces vérifications DB apportent un gain immédiat : vous pouvez prouver si le problème est (a) un hook jamais exécuté, (b) un module non enregistré, (c) une exception, ou (d) une confusion d’alias.

Exploiter un hook dynamique proprement : contrat d’entrée, perf, et tests

L’erreur classique sur les hooks dynamiques est de les traiter comme des hooks métier “haut niveau”. actionObjectProductUpdateAfter déclenche sur toute mise à jour du modèle : BO, import, Webservice, script CLI, module tiers… Donc : pas d’hypothèse UI, pas d’accès direct à Tools::getValue() comme source de vérité, et surtout pas d’écriture en base non protégée. Travaillez à partir de l’objet fourni, et rendez votre code idempotent.

Exemple de handler robuste (PrestaShop 8.1/9.x, PHP 8.1+) :

public function hookActionObjectProductUpdateAfter(array $params): void
{
    $product = $params['object'] ?? null;
    if (!$product instanceof Product || (int) $product->id <= 0) {
        return;
    }

    // Exemple : pousser un recalcul en async plutôt que bloquer la requête
    // (queue maison, cron, ou système externe)
    $this->enqueueReindex((int) $product->id);
}

Trois bonnes pratiques qui évitent 80% des incidents sur ce type de hook :

  • Limiter le travail synchrone : si vous faites une indexation, un export, un appel API, ou une recomposition de cache, préférez une file (même rudimentaire) + un cron. Un hook dynamique se déclenche souvent au pire moment (import massif, flush cache, batch nocturne).
  • Dédupliquer : si un même produit est mis à jour 3 fois pendant un import (prix, puis stock, puis champs traduits), ne lancez pas 3 recalculs identiques. Un simple mécanisme de “coalescing” (une entrée unique par id_product pendant X minutes) suffit souvent.
  • Éviter les effets de bord invisibles : si votre hook modifie l’objet (ex. normalisation) et ré-écrit, documentez-le et tracez-le, sinon la boutique devient difficile à expliquer (“pourquoi cette valeur revient toujours à X ?”).

Sur la perf : considérez qu’un hook dynamique peut se déclencher en rafale (import catalogue, synchro ERP). Si votre handler fait 3 requêtes SQL par produit, vous venez de créer une bombe. Mesurez et limitez : batch, déduplication, queue, et surveillance. L’article interne “Audit performance PrestaShop : méthode en 6 étapes reproductibles” est un bon cadre pour objectiver le coût ( audit performance — article interne ). Et si vous suspectez du cache/ressources, recoupez côté infra (Redis, OPcache) plutôt que d’optimiser micro (ex. Redis : Redis & cache — article interne).

Pour la maintenabilité, traitez vos hooks comme des interfaces : typage, garde-fous, et tests. PrestaShop n’offre pas un contrat formel de payload pour chaque hook, donc vous compensez par (1) assertions, (2) logs exploitables en staging, (3) analyse statique. Une chaîne outillée PHPStan/Rector réduit les régressions quand vous montez de version (cadre général : PHPStan & Rector — article interne).

Et si vous faites du Symfony dans votre module, rappelez-vous que l’écosystème “events” existe aussi : « The EventDispatcher component implements the Mediator pattern » (Symfony Docs, EventDispatcher). Dans certains cas BO, un event Symfony est plus stable qu’un hook legacy, notamment si le point d’extension est dans une couche Symfony “pure” (controllers/services) plutôt que dans le legacy.

Checklist terrain : rechercher et valider un hook dynamique sans perdre une journée

Commencez par verrouiller le contexte : version exacte de PrestaShop (8.1.x vs 9.1), version PHP, thème, liste des modules, et scénario reproductible. Sans ça, vous allez “trouver un hook” qui n’est pas exécuté chez vous (ou pas avec le même payload). Sur les boutiques à trafic, faites ces manipulations en staging ; si vous devez tester sur prod, encadrez (fenêtre courte, rollback, logs) — sinon vous finissez en incident, typiquement un 503 en charge (méthode de diagnostic serveur : erreur HTTP 503 — article interne).

Ensuite, faites une recherche en entonnoir : (1) templates ({hook h=...}) sur la page concernée, (2) contrôleur concerné (legacy/Symfony), (3) modèle concerné (ObjectModel). Si l’action touche la persistance, allez directement lire ObjectModel et déduisez le nom du hook dynamique. Si l’action touche un écran BO Symfony (grids, forms), cherchez d’abord les hooks documentés (ex. actionProductGridDataModifier) avant d’inventer une interception via ObjectModel.

Pour rendre la démarche vraiment “terrain”, voici une checklist courte (à dérouler dans l’ordre) :

  • [ ] Je peux reproduire (même donnée d’entrée, même contexte shop, même profil BO si pertinent).
  • [ ] J’identifie le type d’événement : UI (grille/form), persistance (add/update/delete), ou rendu FO (display).
  • [ ] Je fais un scan code :
  • [ ] rg "Hook::exec\(" sur le périmètre concerné,
  • [ ] rg "actionObject" si suspect ObjectModel,
  • [ ] rg "{hook\s+h=" si FO.
  • [ ] Je fais un scan modules : rg "registerHook\(" + rg "hookAction" pour voir ce qui existe déjà.
  • [ ] Je valide en exécution réelle :
  • [ ] Xdebug pour voir le nom final et le payload,
  • [ ] ou instrumentation temporaire si je veux une liste exhaustive sur un parcours.
  • [ ] Je confirme en base :
  • [ ] ps_hook_module (enregistrement),
  • [ ] exceptions éventuelles,
  • [ ] alias.
  • [ ] J’implémente “minimal et sûr” : handler idempotent, sans I/O lourd, avec garde-fous perf.
  • [ ] Je mesure (temps/SQL) et je corrèle si besoin via slow query log (slow query log — article interne).

Enfin, validez par exécution réelle : Xdebug pour la stack, ou instrumentation temporaire de Hook::exec() pour la liste des hooks déclenchés sur votre parcours. Une fois le hook identifié, vérifiez l’enregistrement en base (ps_hook_module) et la présence d’exceptions. Puis seulement, implémentez : handler minimal, idempotent, sans I/O lourd. La valeur ajoutée d’un bon diagnostic “hooks dynamiques” n’est pas de connaître par cœur une liste : c’est de savoir prouver quel hook est exécuté, avec quel payload, et à quel coût CPU/SQL.


À lire aussi