Vos produits passent bien « actifs » côté PrestaShop après une levée d’embargo, vos jobs de synchro ERP sont tous au vert… et pourtant ces produits restent archivés dans l’ERP tant que personne ne fait un refresh manuel. En base, tout semble cohérent : active = 1. Aucune erreur, aucune alerte. C’est exactement le genre de bug qui peut survivre des mois en production, parce qu’il ne casse rien de visible. Il casse juste un événement que personne ne pense à aller chercher.
Ce que vous allez apprendre :
- Pourquoi un
UPDATESQL « qui marche » peut casser tout l’écosystème applicatif autour d’un produit - Comment un produit publié peut rester invisible côté ERP sans qu’aucun job n’échoue
- La preuve technique qui trahit le contournement :
date_updfigé alors quedate_addchange - Deux stratégies de correction propre, et comment choisir entre le quick fix et la refonte
Ce cas, on l’a diagnostiqué récemment chez un client e-commerce. Le scénario est intéressant parce qu’il ne ressemble pas à un bug : aucune exception, aucun log d’erreur, des données en base parfaitement plausibles. Et pourtant, une brique entière de l’écosystème ne recevait jamais l’information. On vous raconte la démarche complète, du symptôme invisible jusqu’à la correction.
Dans PrestaShop, changer l’état d’un produit en base ne suffit pas. Si d’autres briques dépendent des hooks, il faut passer par le bon chemin applicatif — sinon vous mettez à jour la donnée, mais pas l’événement.
Le symptôme : un produit publié, mais archivé côté ERP
Le contexte fonctionnel est classique. Le client gère des sorties produit avec embargo : certaines références ne doivent devenir visibles qu’à partir d’une date précise. Un module métier custom — un module de publication planifiée — se charge de basculer ces produits en active = 1 au moment voulu.
Côté PrestaShop, tout fonctionne. À l’heure dite, les produits passent actifs, apparaissent sur le front, sont commandables. Personne ne se plaint de ce côté.
Le problème vient de l’ERP, un système de gestion de type Odoo synchronisé avec la boutique. Les produits fraîchement publiés y restaient obstinément archivés. Pas tous, mais systématiquement ceux qui passaient par la publication planifiée. La seule façon de les faire remonter : déclencher un refresh manuel de la synchro, à la main, après chaque levée d’embargo.
👉 Le symptôme observable, c’est ça : un produit actif côté PrestaShop, mais archivé côté ERP, qui ne se débloque qu’avec une intervention humaine. Une tâche récurrente, pénible, et surtout impossible à oublier sous peine de vendre un produit que l’ERP considère comme inexistant.
Pourquoi ce bug ne saute pas aux yeux
C’est tout l’enjeu de ce retour d’expérience. Si vous ouvrez la base de données et que vous regardez la ligne du produit, vous ne voyez aucun problème. La colonne active vaut bien 1, dans ps_product comme dans ps_product_shop. La donnée est cohérente.
Du coup, le réflexe naturel — « le produit est-il bien actif en base ? » — répond « oui ». Et on cherche le bug ailleurs : côté ERP, côté mapping, côté contrat de synchronisation. On peut perdre des jours à auditer le mauvais bout de la chaîne.
Le piège, c’est précisément que la donnée est correcte. Le bug ne se trouve pas dans l’état du produit, mais dans l’événement qui aurait dû accompagner ce changement d’état. Et un événement manquant, ça ne se voit ni dans une table, ni dans un SELECT.
La preuve en production : la donnée bouge, l’événement non
Quand un symptôme est invisible en base, il faut aller chercher les traces ailleurs : les timestamps internes, les hooks, les logs de synchro. C’est là que les choses deviennent parlantes.
1. Le timestamp qui trahit le contournement
Premier indice, et il est décisif. En comparant les colonnes de date sur les produits concernés, on observe une anomalie nette :
date_adda bien évolué — le produit a été touché récemment ;date_upd, en revanche, est resté figé à une date antérieure.
👉 Or, dans PrestaShop, toute mise à jour d’un produit passant par le chemin standard rafraîchit automatiquement date_upd. C’est ObjectModel qui s’en charge, en interne, à chaque update(). Si la donnée a manifestement changé mais que date_upd n’a pas bougé, c’est qu’on n’est pas passé par ObjectModel. Quelque chose a écrit directement en base.
Ce détail, à lui seul, oriente tout le diagnostic. Le timestamp est le témoin silencieux du chemin emprunté par l’écriture.
2. Aucun webhook utile émis après la publication
Deuxième vérification : qu’est-ce qui est émis au moment de la publication ? La synchro vers l’ERP repose sur des événements déclenchés par les hooks produit de PrestaShop. Quand un produit est mis à jour normalement, un hook part, et la machinerie de synchro s’enclenche.
En traçant ce qui se passait au moment précis de la levée d’embargo, on constate qu’aucun webhook utile n’était émis. Le produit passait actif, mais l’écosystème applicatif n’en était jamais informé. Silence radio.
3. Le faux positif le plus piégeux : des jobs ERP « en succès »
Et voici le détail qui explique pourquoi le bug avait survécu si longtemps. Côté ERP, les jobs de synchronisation étaient tous en succès. Aucun échec, aucune file d’erreur, aucune alerte.
C’est logique, et c’est cruel : un job ne peut traiter que ce qu’on lui envoie. Comme aucun événement n’était émis pour ces produits, il n’y avait tout simplement rien à traiter. Les jobs tournaient à vide sur ces références — et un job qui n’a rien à faire termine en succès.
Un job de synchro « tout vert » ne prouve pas que tout est synchronisé. Il prouve seulement que tout ce qui a été reçu a été traité sans erreur. Un événement jamais émis ne génère jamais d’échec.
👉 C’est le piège classique de l’événementiel : on surveille les erreurs, pas les absences. Un événement manquant est invisible par construction.
Le diagnostic dans le code source
Les trois preuves convergeaient vers une seule hypothèse : le produit était publié en contournant le chemin standard de mise à jour. Restait à le confirmer dans le code du module métier.
Ce que faisait le module : un UPDATE SQL brut
En ouvrant le module de publication planifiée, on est tombé sur ceci — la bascule des produits se faisait directement en SQL :
// Dans le module de publication planifiée — le chemin fautif
$sql = 'UPDATE `'._DB_PREFIX_.'product`
SET `active` = 1
WHERE `id_product` IN ('.implode(',', $idsToPublish).')';
Db::getInstance()->execute($sql);
$sqlShop = 'UPDATE `'._DB_PREFIX_.'product_shop`
SET `active` = 1
WHERE `id_product` IN ('.implode(',', $idsToPublish).')';
Db::getInstance()->execute($sqlShop);
Le code « fait le job » au sens le plus littéral : il met bien active = 1 dans ps_product et ps_product_shop. La donnée est correcte. Et c’est précisément pour ça que le bug est passé inaperçu si longtemps.
Mais en écrivant directement en base, ce code court-circuite toute la couche applicative de PrestaShop.
Pourquoi ça casse toute la chaîne de hooks
Pour comprendre ce qui est perdu, il faut regarder ce que fait normalement PrestaShop quand on met à jour un produit proprement. Le bon chemin passe par ObjectModel::update() (via Product->save() ou Product->update()). Et cette méthode ne se contente pas d’écrire en base : elle déclenche, en fin de mise à jour, le hook actionProductUpdate — ainsi que le hook générique actionObjectProductUpdateAfter.
Ce sont ces hooks qui font vivre tout l’écosystème autour du produit :
- réindexation de la recherche ;
- invalidation des caches concernés ;
- et surtout, dans notre cas, l’émission de l’événement de synchro qui prévient l’ERP qu’un produit a changé.
👉 En contournant ObjectModel::update(), l’UPDATE SQL brut empêche actionProductUpdate de se déclencher. Pas de hook, donc pas de webhook, donc pas d’événement vers l’ERP. La donnée change, mais personne n’est prévenu. Le produit existe pour PrestaShop, et reste un fantôme pour tout ce qui écoute les hooks.
L’enseignement est réutilisable bien au-delà de ce module : dans PrestaShop, modifier l’état d’un objet en base ne suffit pas si d’autres briques dépendent des hooks. Le hook n’est pas un effet de bord cosmétique de l’update() : c’est le point d’entrée de tout un écosystème événementiel.
La correction : deux stratégies, deux philosophies
Une fois la cause racine identifiée, deux corrections étaient possibles. Elles ne se valent pas, et le choix dépend du contexte. On les a évaluées toutes les deux.
Stratégie 1 — Repasser par le chemin applicatif (la voie propre)
La correction la plus saine consiste à arrêter d’écrire en SQL et à repasser par l’objet produit. On charge chaque Product, on positionne active, et on appelle save() (ou update()) :
// Le bon chemin : ObjectModel propage toute la chaîne de hooks
foreach ($idsToPublish as $idProduct) {
$product = new Product((int) $idProduct);
$product->active = true;
$product->save(); // déclenche actionProductUpdate + actionObjectProductUpdateAfter
}
L’avantage est total sur le fond : date_upd est rafraîchi, la réindexation se fait, le cache s’invalide, et surtout l’événement de synchro part vers l’ERP. Toute la chaîne de hooks est propagée, exactement comme si un humain avait modifié le produit dans le back-office.
👉 Le coût, c’est un update complet par objet. Là où l’UPDATE SQL traitait des centaines de produits en une requête, on charge ici chaque produit, on déclenche toute la machinerie applicative, puis on enchaîne. Sur une levée d’embargo de quelques dizaines de produits, c’est négligeable. Sur des publications de masse de plusieurs milliers de références d’un coup, il faut anticiper le temps d’exécution et, le cas échéant, traiter par lots ou en tâche de fond.
Stratégie 2 — Déclencher explicitement le hook (le correctif minimal)
Si l’on veut un correctif moins intrusif — par exemple parce que l’UPDATE SQL en masse est volontairement conservé pour ses performances — il existe une voie intermédiaire : garder l’écriture SQL, mais émettre explicitement le hook manquant après coup.
// Correctif minimal : on garde l'UPDATE SQL, on rejoue le hook à la main
Db::getInstance()->execute($sql);
Db::getInstance()->execute($sqlShop);
foreach ($idsToPublish as $idProduct) {
Hook::exec('actionProductUpdate', [
'id_product' => (int) $idProduct,
'product' => new Product((int) $idProduct),
]);
}
L’événement repart, l’ERP est prévenu, et on conserve la rapidité de l’écriture SQL groupée. C’est le correctif que l’on déploie volontiers quand il faut rétablir le flux rapidement sans refondre le module.
Mais soyons honnêtes sur ses limites. Cette approche est plus fragile :
- elle ne rafraîchit pas
date_upd(l’écriture reste en SQL brut), donc le témoin du problème subsiste ; - elle suppose de connaître exactement la signature attendue du hook par les modules qui l’écoutent ;
- elle ne couvre que le hook que vous rejouez explicitement — si une autre brique dépend d’un autre effet de
update(), elle reste, elle, court-circuitée.
👉 Autrement dit : la stratégie 2 répare le symptôme prioritaire, la stratégie 1 supprime la cause racine.
Notre arbitrage : quick fix d’abord, refonte ensuite
Dans la pratique, ces deux stratégies ne s’opposent pas, elles se séquencent. On déploie le Hook::exec() comme correctif rapide pour stopper l’hémorragie opérationnelle — plus de réactivation manuelle côté ERP. Puis on planifie la bascule vers Product->save() comme dette technique à solder, en validant au passage le comportement en masse.
En attendant le fix : le coût d’un événement manquant
Tant que la correction n’était pas déployée, l’équipe vivait avec un contournement bien réel : après chaque levée d’embargo, quelqu’un devait réactiver manuellement les produits côté ERP. Une manipulation pour « forcer » la synchro que le code aurait dû déclencher tout seul.
Ça paraît anodin. Ça ne l’est pas :
- c’est une tâche récurrente qui mobilise du temps humain à chaque sortie produit ;
- c’est une étape oubliable — et un oubli signifie un produit vendable côté front mais ignoré de l’ERP, avec tout ce que ça implique en gestion de stock et en logistique ;
- c’est une fausse impression de fiabilité : « la synchro marche » parce qu’on la rattrape à la main, jusqu’au jour où on ne la rattrape pas.
👉 Le vrai coût d’un événement manquant, ce n’est pas le bug en lui-même. C’est l’opération manuelle récurrente qu’il impose, et le risque silencieux qu’il fait peser à chaque fois qu’on l’oublie.
Comment valider que le fix fonctionne vraiment
Corriger, c’est bien. Vérifier que la correction a rétabli l’événement, c’est indispensable. Dans ce genre de bug, on ne se fie pas à « ça a l’air de marcher » — on vérifie là où le symptôme se cachait :
- Le timestamp. Après publication,
date_upddoit désormais bouger (au moins avec la stratégie 1). S’il reste figé, c’est qu’on est encore en SQL brut. - Les logs de webhook. Un événement utile doit partir au moment de la publication. C’est la preuve directe que le hook s’est bien déclenché.
- L’état côté ERP. Le produit doit remonter actif sans refresh manuel. C’est le critère final : si plus personne ne touche l’ERP à la main, le flux est rétabli.
Si l’un de ces trois points cloche, c’est que le chemin standard est encore contourné quelque part. Le bon réflexe est de ne pas se contenter de « la donnée est bonne en base » — c’est précisément ce raisonnement qui avait masqué le bug au départ.
Notre avis chez Mintfull
Ce cas est exemplaire d’une catégorie de bugs qu’on rencontre régulièrement : ceux où la donnée est correcte mais l’événement manque. Ils sont sournois parce qu’ils résistent au diagnostic le plus naturel — « la base dit-elle la vérité ? » — alors que le problème est ailleurs, dans la mécanique applicative qui aurait dû accompagner l’écriture.
Un
UPDATESQL bien ciblé est tentant : rapide, lisible, efficace. Mais sur PrestaShop, il vous fait sortir de l’autoroute des hooks. Tout ce qui dépendait de ces hooks cesse silencieusement de fonctionner — et le silence est précisément ce qui rend ces régressions si dures à attraper.
Notre règle, sur tous les projets où des briques externes dépendent du cycle de vie des objets : on écrit via ObjectModel, pas en SQL brut, sauf cas très précis et parfaitement maîtrisé. Le SQL direct a sa place pour des opérations de masse purement techniques, sans effet de bord attendu. Dès qu’un hook, un webhook ou un indexeur est dans la boucle, on repasse par le chemin standard — ou on rejoue explicitement les événements concernés, en assumant la fragilité de ce choix.
C’est aussi un sujet de méthode : sur ce type d’architecture, on instrumente la synchro pour qu’un événement manquant soit aussi visible qu’un job en échec. Surveiller uniquement les erreurs, c’est se condamner à découvrir les absences trop tard. Si le sujet de la robustesse des données et des performances vous parle, notre article sur le back-office PrestaShop lent creuse la même philosophie : mesurer avant de conclure, et ne jamais se fier à un indicateur qui « semble » au vert.
FAQ : SQL brut, hooks et synchro produit dans PrestaShop
Un UPDATE SQL direct est-il toujours une mauvaise pratique sur PrestaShop ?
Non, pas systématiquement. Le SQL direct est légitime pour des opérations de masse purement techniques, sans effet de bord applicatif attendu (corrections ponctuelles, scripts de maintenance maîtrisés). Le problème apparaît dès qu’une brique externe — synchro ERP, indexeur, cache, webhook — dépend des hooks déclenchés par ObjectModel. Dans ce cas, l’UPDATE brut court-circuite l’événement et casse la chaîne silencieusement.
Quel hook se déclenche quand on met à jour un produit proprement ?
Une mise à jour via Product->save() ou Product->update() déclenche actionProductUpdate, ainsi que le hook générique actionObjectProductUpdateAfter. Ce sont eux que les modules de synchro, de réindexation ou de cache écoutent pour réagir à un changement de produit. Un UPDATE SQL direct ne déclenche ni l’un ni l’autre.
Comment savoir si un produit a été modifié en contournant ObjectModel ?
Le meilleur indice est le timestamp date_upd. PrestaShop le rafraîchit automatiquement à chaque mise à jour passant par ObjectModel. Si la donnée d’un produit a manifestement changé mais que date_upd est resté figé, c’est le signe qu’une écriture directe en base a contourné le chemin standard. C’est exactement la trace qui nous a mis sur la piste dans ce cas.
Pourquoi les jobs de synchro ERP étaient-ils « en succès » alors que rien ne remontait ?
Parce qu’un job ne traite que ce qu’on lui transmet. Comme aucun événement n’était émis au moment de la publication, il n’y avait rien à synchroniser pour ces produits : les jobs tournaient à vide et terminaient donc sans erreur. C’est le piège de l’événementiel : on surveille les échecs, jamais les absences. Un événement jamais émis ne génère jamais d’alerte.
Vous soupçonnez qu’une mise à jour « qui marche en base » casse silencieusement une synchro, un cache ou un indexeur sur votre boutique ?
👉 Notre agence PrestaShop diagnostique précisément ce type de régression invisible : on remonte du symptôme jusqu’au chemin de code fautif, on corrige proprement, et on instrumente vos flux pour qu’un événement manquant devienne aussi visible qu’une erreur. Si vous préparez par ailleurs une montée de version, notre point complet sur PrestaShop 9 vous aidera à anticiper les impacts sur vos modules custom.
Parce qu’un produit publié qui n’arrive jamais dans votre ERP, ce n’est pas un détail technique. C’est une vente que votre système ne voit pas venir.


