Table des matières :
- Ce que fait exactement le hook
actionProductGridDataModifier(et ce qu’il ne fait pas) - Où est déclenché le hook dans le cœur (PrestaShop 8.x/9.x) : chemin d’exécution et stratégie de repérage
- Paramètres du hook et structure des données :
GridData, records et invariants à respecter - Exemple concret (module) : enrichir la grille produit avec un indicateur calculé sans casser l’admin
- Afficher la donnée : coupler
actionProductGridDataModifieravecactionProductGridDefinitionModifier - Points durs en production : perf, multiboutique, ACL, export et compatibilité 1.7 → 9.1
- Checklist de validation rapide (dev → staging → prod) pour
actionProductGridDataModifier
Ce que fait exactement le hook actionProductGridDataModifier (et ce qu’il ne fait pas)
Dans PrestaShop 8.x et 9.x, la liste des produits du Back-Office n’est plus une simple boucle Smarty alimentée par un SELECT “maison”. Elle passe par le composant Grid (SearchCriteria → QueryBuilder → DataFactory → Grid), ce qui standardise pagination, tri, filtres et export. Le hook actionProductGridDataModifier intervient après l’exécution SQL et la constitution de l’objet de données du grid, donc au moment où vous avez déjà une collection de lignes (records) prêtes à être rendues.
Concrètement, ce hook est fait pour enrichir ou transformer les records (ajouter un champ calculé, normaliser un format, injecter un indicateur technique, etc.) sans toucher à la requête. Si votre besoin implique un nouveau champ triable/filtrable (ex. un champ SQL ou un agrégat), ce hook seul est insuffisant : il faut généralement combiner avec actionProductGridQueryBuilderModifier (pour ajouter jointures/sélections) et/ou actionProductGridDefinitionModifier (pour déclarer la colonne, ses options, son rendu).
Pour éviter les malentendus fréquents côté dev (et gagner du temps en debug), voici une grille de lecture rapide “besoin → hook(s)” :
| Besoin | Hook principal | Pourquoi |
|---|---|---|
| Ajouter une valeur calculée non triable/non filtrable (ex. bool “à vérifier”, libellé QA) | actionProductGridDataModifier |
Post-traitement PHP sur les records déjà récupérés |
| Afficher une nouvelle colonne dans la grille | actionProductGridDefinitionModifier (+ data) |
La définition décrit l’UI (colonne, label, options) |
| Rendre le champ triable/filtrable à grande échelle | actionProductGridQueryBuilderModifier (+ definition) |
Le tri/pagination se font côté SQL (QueryBuilder) |
| Optimiser un indicateur coûteux (catalogue volumineux) | Pré-calcul / table d’index + QueryBuilder | Le “calcul à la volée” en BO devient vite un goulot |
Il faut aussi comprendre sa limite principale : modifier les données après coup ne rend pas magiquement ces données compatibles avec le tri/pagination côté SQL. Exemple classique : si vous calculez un “score” en PHP et que vous l’affichez dans une colonne, vous ne pourrez pas trier proprement par ce score à grande échelle sans le produire dans la requête (ou sans un mécanisme d’indexation en amont). C’est un choix architectural du Grid : le tri/pagination s’appliquent dans le QueryBuilder, pas sur le tableau final.
Enfin, un point très pratique : dans ce hook, vous êtes dans un contexte Back-Office. Tout ajout de latence se voit immédiatement (et coûte de la productivité aux équipes qui gèrent le catalogue). Une règle simple à viser : une requête groupée + un mapping en PHP, plutôt qu’une requête par ligne.
Référence utile (sur la notion de référence en PHP, importante ici car PrestaShop passe certains paramètres par référence) : « References in PHP are a means to access the same variable content by different names. » — PHP Manual, References
https://www.php.net/manual/en/language.references.php
Où est déclenché le hook dans le cœur (PrestaShop 8.x/9.x) : chemin d’exécution et stratégie de repérage
En pratique, ce hook est déclenché lors de la construction du grid identifié par product (grid id). Le nom final du hook suit le pattern action{GridId}GridDataModifier, d’où actionProductGridDataModifier. Le point d’appel se situe dans le pipeline Grid, typiquement dans la factory qui instancie la définition, construit la requête, récupère les données, puis laisse une fenêtre de modification via hooks.
Le meilleur réflexe (compat 8.0 → 9.1) est d’éviter les suppositions sur le chemin exact de fichier (ça bouge entre branches), et de s’appuyer sur la recherche dans le code source :
# À la racine PrestaShop
rg -n "actionProductGridDataModifier" -S
# Ou plus générique (utile si vous cherchez le mécanisme de composition du nom)
rg -n "GridDataModifier" src/ classes/ -S
Cherchez ensuite un appel du type Hook::exec() (ou une couche équivalente) qui exécute le hook avec un tableau de paramètres. Dans la majorité des cas, vous verrez explicitement que la clé data est passée par référence (ex. ['data' => &$data]) : c’est ce qui rend possible le remplacement du GridData depuis votre module.
Côté “qui appelle le grid”, la liste produits BO passe par un contrôleur Symfony (Sell/Catalog) qui demande un grid via un service/factory. Pour remonter le chemin d’exécution sans “printf debugging” :
- activez le profiler Symfony en environnement de dev,
- ou utilisez Xdebug pour confirmer que vous arrivez bien dans votre hook après la data factory (et pas avant le QueryBuilder).
Si vous avez besoin d’un rappel sur la séparation dev/prod et les pièges classiques de log/affichage d’erreurs (très utile quand votre hook casse l’admin), voir l’article interne : Gestion d’erreur PHP : bonnes pratiques et configuration développement/production.
Dernier point : votre capacité à tracer proprement dépend aussi de votre stack (versions PHP, CLI, mode debug, etc.). Pour cadrer l’environnement avant d’incriminer le hook, gardez sous la main : Nouveautés dev et compatibilité PHP de PrestaShop 9.1.
Paramètres du hook et structure des données : GridData, records et invariants à respecter
Le handler de module attendu est :
public function hookActionProductGridDataModifier(array $params): void
Dans $params, vous récupérez au minimum :
data: l’objet représentant les données du grid (en 8/9, unGridDataou équivalent), contenant la collection de records, le total et parfois la requête “debug”.search_criteria: les critères (page, offset, limit, tri, filtres). C’est utile pour adapter votre enrichissement (ex. seulement sur certaines pages) ou éviter du calcul inutile.
À quoi ressemblent “les records” côté produit ?
Sans figer un schéma exact (il peut varier selon versions/modules), un record de grille produit contient en général des clés proches de :
id_productnamereferenceprice/price_final(selon contexte)activequantity(selon la grille/config)id_category_default,position, etc.
Votre objectif est d’ajouter une clé (ex. mymodule_stock_negative) sans casser ce qui existe.
Les invariants (à respecter pour ne pas casser pagination/actions)
Le point qui piège : vous ne modifiez pas un simple tableau, vous intervenez dans un pipeline où l’admin s’attend à des invariants stables :
- Le nombre de records doit rester cohérent avec la pagination (évitez de supprimer des lignes “à la volée”, sauf si vous maîtrisez l’impact UX + total).
- Le total (
recordsTotal) doit rester aligné avec la requête de comptage ; si vous le modifiez en PHP, vous cassez pagination et parfois l’export. - Les clés standard des records (
id_product,name,active, etc.) sont consommées par des renderers et des actions (bulk, preview, édition). Si vous les renommez/écrasez, vous créez des bugs difficiles à diagnostiquer.
Dans la plupart des cas, la bonne approche consiste à : (a) extraire les records, (b) les enrichir en ajoutant une nouvelle clé, (c) reconstruire un GridData avec les records enrichis, sans toucher au total.
Le piège n°1 : le N+1 (et comment l’éviter proprement)
Évitez le N+1 : si vous faites une requête par produit sur une page de 50 lignes, vous venez d’ajouter 50 requêtes à chaque chargement de l’admin. Préférez :
- collecter les
id_productde la page, - faire une seule requête avec
IN (...), - remapper en PHP (tableau associatif),
- enrichir les records.
Sur l’aspect “ne pas concaténer n’importe quoi dans du SQL”, même si ici on parle d’IDs entiers, gardez les bonnes pratiques en tête (paramétrage, cast, validation). Une référence utile et reconnue côté sécurité applicative : OWASP SQL Injection Prevention Cheat Sheet
Optimisation simple (souvent oubliée) : exploiter search_criteria
Si votre enrichissement est coûteux, utilisez search_criteria pour réduire l’impact :
- ne calculer que si
limit≤ 50 (ou votre standard), - court-circuiter certains traitements si un filtre exclut déjà les cas pertinents,
- éviter les calculs inutiles en contexte d’export si votre colonne n’est pas exportée (à tester selon votre BO).
Ce genre de micro-optimisation est particulièrement utile sur des catalogues volumineux, où la grille produit est ouverte des dizaines de fois par jour par l’équipe.
Sur le problème N+1, la règle n’est pas “ne jamais requêter”, c’est “ne jamais requêter dans une boucle quand une requête groupée fait le job”. Pour objectiver, mesurez : nombre de requêtes et temps cumulé DB (profiler Symfony, slow log MySQL/MariaDB).
Exemple concret (module) : enrichir la grille produit avec un indicateur calculé sans casser l’admin
Le cas d’usage réaliste : vous voulez afficher, dans la grille produits BO, un indicateur technique “stock négatif” ou “incohérence stock/active”, ou un “has_images” pour accélérer un contrôle qualité. Ce type d’indicateur n’a pas forcément vocation à être triable/filtrable dans un premier temps : actionProductGridDataModifier est un bon point d’entrée.
Mini-scénario (très courant en maintenance) : un catalogue importé depuis un ERP génère des quantités négatives sur certaines références (retours, ajustements, erreurs de feed). L’équipe catalogue veut une vue rapide “à corriger” directement depuis la liste produits BO, sans ouvrir chaque fiche.
Pré-requis et contexte de versions
- PrestaShop : 8.1+ ou 9.1 (les grids Symfony sont stables sur ces branches ; 1.7 est partiellement compatible mais nécessite plus d’adaptations)
- PHP : selon votre cœur (sur 9.1, 8.1–8.5 sont annoncés) — validez avec votre environnement (CI + prod)
- Accès : lecture DB en BO + permissions employé (ne contournez pas l’ACL avec des requêtes “hors contexte” si vous exposez des infos sensibles)
Squelette minimal du module
Dans mymodule/mymodule.php :
<?php
use PrestaShop\PrestaShop\Core\Grid\Data\GridData;
use PrestaShop\PrestaShop\Core\Grid\Record\RecordCollection;
if (!defined('_PS_VERSION_')) {
exit;
}
class MyModule extends Module
{
public function __construct()
{
$this->name = 'mymodule';
$this->version = '1.0.0';
$this->author = 'Vous';
$this->tab = 'administration';
$this->bootstrap = true;
parent::__construct();
$this->displayName = 'MyModule';
$this->description = 'Enrichit la grille produit BO';
}
public function install(): bool
{
return parent::install()
&& $this->registerHook('actionProductGridDataModifier');
}
public function hookActionProductGridDataModifier(array $params): void
{
if (empty($params['data'])) {
return;
}
/** @var GridData $data */
$data = $params['data'];
// Selon versions, getRecords() renvoie une RecordCollection.
$records = method_exists($data->getRecords(), 'all')
? $data->getRecords()->all()
: (array) $data->getRecords();
// 1) Collecter les IDs produits de la page courante
$productIds = [];
foreach ($records as $row) {
if (!empty($row['id_product'])) {
$productIds[] = (int) $row['id_product'];
}
}
$productIds = array_values(array_unique($productIds));
if (!$productIds) {
return;
}
// 2) Requête groupée (éviter N+1)
// Note : en Back-Office, si vous avez une réplication SQL, _PS_USE_SQL_SLAVE_
// peut renvoyer des données légèrement en retard (stock, prix, etc.).
// À ajuster selon votre infra et votre besoin de "fraîcheur".
$idShop = (int) $this->context->shop->id;
// Sécuriser : IDs castés en int => OK pour construire un IN(...) “propre”.
$idsSql = implode(',', array_map('intval', $productIds));
$sql = "
SELECT ps.id_product, COALESCE(sa.quantity, 0) AS quantity
FROM " . _DB_PREFIX_ . "product_shop ps
LEFT JOIN " . _DB_PREFIX_ . "stock_available sa
ON sa.id_product = ps.id_product
AND sa.id_product_attribute = 0
AND sa.id_shop = ps.id_shop
WHERE ps.id_shop = {$idShop}
AND ps.id_product IN ({$idsSql})
";
$rows = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
$qtyByProduct = [];
foreach ($rows as $r) {
$qtyByProduct[(int) $r['id_product']] = (int) $r['quantity'];
}
// 3) Enrichir les records
foreach ($records as &$row) {
$pid = (int) ($row['id_product'] ?? 0);
$qty = $qtyByProduct[$pid] ?? 0;
// Champ calculé : stock négatif ?
$row['mymodule_stock_negative'] = ($qty < 0);
// Exemple d'enrichissement “bonus” léger (facultatif) :
// un libellé lisible (utile si vous affichez un badge)
$row['mymodule_stock_label'] = ($qty < 0) ? 'Stock négatif' : '';
}
unset($row);
// 4) Réinjecter un nouveau GridData (sans toucher au total)
$params['data'] = new GridData(
new RecordCollection($records),
$data->getRecordsTotal(),
$data->getQuery()
);
}
}
Trois remarques importantes :
- Oui, l’exemple utilise
Db::getInstance()pour rester compatible avec l’écosystème PrestaShop (beaucoup de modules historiques l’utilisent). Sur un module strictement 8/9, vous pouvez préférer DBAL via le container Symfony, mais ça vous engage sur des détails d’injection et de disponibilité. - On enrichit “à plat” le record avec une clé
mymodule_stock_negative. Tant que vous ne modifiez pas les clés core utilisées par les actions/renderer, c’est généralement sûr. - Cet enrichissement ne s’affiche pas tant que vous n’ajoutez pas une colonne dans la définition du grid. C’est normal :
actionProductGridDataModifierne modifie pas l’UI.
Astuce maintenance : si vous soupçonnez un conflit de clé, préfixez systématiquement vos champs (mymodule_...) et évitez les noms génériques (status, flag, label) qui risquent de collisionner avec d’autres modules.
Afficher la donnée : coupler actionProductGridDataModifier avec actionProductGridDefinitionModifier
Modifier les données sans déclarer la colonne ne sert que si un autre rendu exploite la clé (ex. un template override, un endpoint JSON custom, un export). Pour un usage BO standard, il faut ajouter une colonne via actionProductGridDefinitionModifier. La logique est claire : la definition décrit les colonnes, types, options ; la data fournit les valeurs.
La stratégie propre :
actionProductGridDefinitionModifier: ajoute une colonne “Stock négatif” (type booléen ou badge), positionnée où vous voulez.actionProductGridDataModifier: injectemymodule_stock_negative(bool) dans chaque record.- Optionnel : si vous voulez trier/filtrer sur cet indicateur, vous basculez le calcul côté SQL via
actionProductGridQueryBuilderModifier(ou vous le persistez dans une table indexée).
Exemple (pseudo-code) pour la définition (les classes exactes dépendent de la version, mais l’approche reste la même) :
public function hookActionProductGridDefinitionModifier(array $params): void
{
$definition = $params['definition'];
// Objectif :
// - ajouter une colonne lisant la clé 'mymodule_stock_negative'
// - désactiver le tri si la valeur est purement calculée en PHP
// - choisir un rendu simple (bool/badge) pour éviter un override fragile
// Selon version : utilisez un type de colonne "Data"/"Badge"/"Toggle"/"Boolean"
// disponible dans PrestaShop\Core\Grid\Column\Type\Common\...
}
Deux recommandations concrètes (qui évitent beaucoup de “ça marche en dev mais pas en prod”) :
- Rendu minimaliste : si votre indicateur sert à du QA catalogue, un bool/badge suffit. Gardez le lien vers une page de diagnostic pour plus tard (ou faites-le seulement si vous maîtrisez le routing + ACL).
- Ne forcez pas le tri tant que votre valeur est calculée en PHP : vous aurez des tris incohérents (seulement sur la page courante) ou des performances catastrophiques si vous tentez de re-trier en mémoire.
Le point important en maintenance : ne vous battez pas contre les renderers du core. Si vous avez besoin d’un rendu spécifique (badge custom, lien vers une page de diagnostic), faites-le via un type de colonne existant (bool, badge, link…) si disponible, ou assumez un override/rendu custom en sachant que c’est la partie la plus fragile lors des upgrades.
Pour aller plus loin, la doc officielle sur les hooks côté modules (à jour) vaut le détour :
Points durs en production : perf, multiboutique, ACL, export et compatibilité 1.7 → 9.1
Sur un shop multi-boutique, la grille produit BO peut être affichée dans un contexte shop, group ou all-shops selon configuration et permissions. Votre requête d’enrichissement doit respecter ce contexte, sinon vous affichez des indicateurs faux (ex. stock d’un autre shop). Dans l’exemple, on fixe id_shop depuis $this->context->shop->id, ce qui est cohérent en contexte “un shop”. Si votre BO travaille “all shops”, vous devez adapter la logique (agrégation, ou enrichissement par shop), et accepter que la notion de “stock” n’est plus forcément un scalaire unique.
Sur la perf : la grille produit est une page BO critique. Un bon garde-fou consiste à mesurer le coût ajouté : temps SQL et CPU PHP. En pratique, si vous enrichissez 50 lignes, visez une requête groupée O(1) (une requête + un map) et quelques millisecondes PHP.
Un piège fréquent en production (surtout sur des architectures avec réplication) : _PS_USE_SQL_SLAVE_ peut être intéressant pour réduire la charge, mais il peut aussi introduire un décalage entre ce que l’utilisateur vient de modifier (ex. stock) et ce que votre indicateur lit. Pour des indicateurs “d’audit”, ce décalage est souvent acceptable ; pour un indicateur censé être temps réel, privilégiez la source maître.
Sur l’export et les usages secondaires : certains grids réutilisent la même factory/data pipeline pour produire des exports CSV ou des endpoints. Enrichir la data peut donc impacter autre chose que l’affichage HTML (ce qui est parfois souhaité, mais pas toujours). Testez au minimum : (1) affichage paginé, (2) recherche filtrée, (3) tri, (4) export si disponible, (5) actions bulk (activation/désactivation, suppression) pour vérifier que vos clés ajoutées ne collisionnent pas.
Sur l’ACL : même si vous n’affichez “qu’un bool”, demandez-vous si l’indicateur révèle une information sensible (ex. marge, fournisseur, statut interne). Dans ce cas, conditionnez l’enrichissement au profil employé/permission, ou n’exposez pas la colonne à tous les rôles.
Enfin, côté compatibilité : si vous migrez un parc 1.7/8 → 9, la mise en place des hooks grid est un endroit classique où les modules “legacy” cassent (ancien BO ≠ pipeline Grid). Si vous êtes dans une trajectoire d’upgrade, cadrer votre plan de migration et vos adaptations modules est plus rentable que du patch au coup par coup : Guide de migration PrestaShop 9 (1.7/8 → 9) avec 1-Click Upgrade.
Pour réduire le risque en maintenance évolutive, une approche pragmatique consiste à isoler votre enrichissement derrière un “feature flag” (config module) : vous pouvez le désactiver sans déployer en urgence si une mise à jour cœur change un détail de grille.
Checklist de validation rapide (dev → staging → prod) pour actionProductGridDataModifier
En local (mode dev), commencez par valider que le hook est bien exécuté : ajoutez un log ciblé (Monolog si vous êtes côté Symfony, ou PrestaShopLogger/logs modules selon vos habitudes) et contrôlez que l’enrichissement n’apparaît que sur le grid “product”. Si vous utilisez Xdebug, posez un breakpoint dans hookActionProductGridDataModifier() puis suivez la pile d’appel pour confirmer le moment exact (après data factory).
Ensuite, validez les invariants : pagination stable (page 1/2/3), absence d’erreurs PHP/notice (un simple undefined index peut polluer le BO si votre config d’erreur est trop permissive), et volume SQL constant. Si vous voyez “+50 requêtes” sur la page, vous avez un N+1.
Checklist opérationnelle (courte, mais orientée “ça casse en prod”) :
- [ ] Hook exécuté uniquement sur la grille
product(pas sur d’autres grids). - [ ] Aucun écrasement de clés core (
id_product,active, etc.). - [ ] Aucun changement de
recordsTotal. - [ ] Nombre de requêtes stable (idéalement +1) et temps DB maîtrisé.
- [ ] Test catalogue réaliste : 10k+ produits (ou au moins une base de staging proche prod).
- [ ] Test multi-boutique : contexte shop vs all-shops (résultats cohérents).
- [ ] Test avec au moins 2 profils employés (ACL différentes).
- [ ] Test actions bulk (activation/désactivation) + édition produit depuis la grille.
- [ ] Si colonne ajoutée : rendu correct + pas de tri “mensonger” sur valeur calculée.
Enfin, sur staging/prod : surveillez le temps de réponse et l’expérience BO. Sur un back-office, un ajout “acceptable” est généralement < 50 ms au total sur la page (ordre de grandeur, pas une règle absolue) ; au-delà, justifiez-le (et idéalement pré-calculez). Si vous devez ajuster la visibilité des erreurs entre dev/prod pour éviter un BO “blanc” en cas de bug, relisez la stratégie recommandée dans l’article : Configurer proprement la gestion d’erreurs PHP entre dev et production.




