Table des matières :
- Pourquoi l’« état panier » ne doit pas finir dans
ps_configuration - Comment PrestaShop charge la configuration (et pourquoi ça amplifie le problème)
- Symptômes terrain : gonflement de table, lenteurs, bugs “fantômes” en multiboutique
- Stocker correctement un état panier : scopes, persistance et contraintes réelles
- Implémentation (module) : remplacer
Configuration::updateValue()par une tableps_mymodule_cart_state - Migration : détecter, transférer, purger (sans créer un incident prod)
- Garde-fous : empêcher la réintroduction de l’anti-pattern (revue, CI, observabilité)
Pourquoi l’« état panier » ne doit pas finir dans ps_configuration
ps_configuration est conçue pour stocker des paramètres stables (feature flags, clés API, réglages checkout, toggles module) à l’échelle d’une boutique ou d’un shop group. L’« état panier » (au sens runtime state) correspond au contraire à des données volatiles et par utilisateur : étape atteinte dans le tunnel, point relais sélectionné, devis transporteur recalculé, choix de cadeau, données temporaires de personnalisation, etc. Mélanger les deux, c’est accepter qu’un stockage global se comporte comme une session — et ça ne marche pas.
Le premier problème est fonctionnel : Configuration::updateValue() écrit une valeur globalisée (ou scindée par shop en multiboutique), pas une valeur “scopée” par id_cart. Donc soit vous écrasez la valeur à chaque client (effet « dernier écrivant gagne »), soit vous dérivez des clés de configuration du type MYMODULE_CART_STATE_12345… et vous transformez ps_configuration en table de runtime. Dans les deux cas, vous ouvrez la porte à des incohérences : un client voit l’état d’un autre, ou bien l’état persiste trop longtemps et pollue des sessions futures.
Mini-scénario (très courant en France avec les modules de relais type Mondial Relay / Relais Colis) :
- Client A choisit un relais, le module enregistre
PICKUP_POINT_ID=FR-123viaConfiguration::updateValue()(ou équivalent). - Client B arrive 2 secondes après, choisit un autre relais, la valeur globale est écrasée.
- Client A valide sa commande : le relais “change” sans action visible → support, litiges, retours.
Le second problème est architecture/perf. La configuration PrestaShop est chargée massivement et souvent mise en cache. Ajouter des milliers de clés « par panier » augmente la taille du dataset à charger/serializer, multiplie les invalidations de cache et peut déclencher des contentions InnoDB. Sur des boutiques à trafic soutenu, ça se traduit rapidement par une latence qui grimpe sur les pages panier/checkout, puis par une lenteur back-office (chargement des pages qui lisent beaucoup de configuration). Si vous êtes déjà en phase d’audit perf, recoupez ces symptômes avec une méthodologie reproductible (voir : Audit performance PrestaShop : méthode reproductible et preuves terrain).
À retenir : une donnée qui doit expirer (minutes/heures/jours) n’a rien à faire dans une table pensée comme un registre de paramètres.
Comment PrestaShop charge la configuration (et pourquoi ça amplifie le problème)
Dans PrestaShop 8.1 → 9.2 (PHP 8.1+ recommandé ; PHP 8.2/8.3 courant en prod en 2026), l’accès à la configuration passe classiquement par Configuration::get()/Configuration::getMultiple() et les écritures par Configuration::updateValue()/Configuration::deleteByName(). Le cœur charge un ensemble de clés depuis MySQL et les conserve en mémoire/caches applicatifs. La classe est dans le core (référence : référence : Configuration.php sur GitHub). Même si votre module n’appelle qu’une clé, le runtime PrestaShop a tendance à manipuler des lots, notamment en back-office.
Le point critique : toute écriture de configuration invalide des caches (selon votre stack : cache FS, APCu, Redis via cache adapter, etc.). Sur une page panier où vous écrivez plusieurs fois (ex. recalcul shipping, choix point relais, surcoûts, cadeaux), vous introduisez un pattern “write-heavy” sur une table qui n’est pas faite pour ça. Et l’invalidation n’est pas gratuite : elle force les processus PHP-FPM à recharger/rehydrater la configuration. Si vous avez optimisé OPcache/PHP-FPM/Redis mais que vous conservez ce pattern, vous neutralisez une partie des gains (voir : Lenteur back-office PrestaShop : OPcache, PHP-FPM et Redis pour accélérer et Cache PrestaShop : stratégie 2026 OPcache, Redis, Varnish et CDN).
Enfin, ps_configuration est structurellement indexée pour des lectures par name/shop, pas pour des requêtes analytiques sur des millions de clés “dynamiques”. En pratique, vous créez une table chaude en écriture, partagée par tous les workers, avec des mises à jour fréquentes. La documentation MySQL (InnoDB) rappelle que les deadlocks font partie du fonctionnement d’une base transactionnelle et qu’ils doivent être anticipés côté applicatif (MySQL 8.0 Reference Manual, InnoDB deadlocks). Ajouter un flux d’écritures inutiles sur une table globale augmente mécaniquement la probabilité d’attendre des verrous ou de rencontrer des deadlocks (même si InnoDB en “résout” en abandonnant une transaction).
Concrètement, en checkout :
- vous multipliez les
UPDATE/INSERTsur une table centrale ; - vous multipliez les invalidations de cache ;
- vous augmentez la variance de latence (p95/p99), celle qui “fait mal” en conversion.
Symptômes terrain : gonflement de table, lenteurs, bugs “fantômes” en multiboutique
Le symptôme le plus simple à objectiver : le nombre de lignes et la taille de ps_configuration. Sur une boutique saine, vous êtes typiquement dans quelques milliers de lignes. Quand un module stocke un état panier par clé, on voit des dizaines/centaines de milliers de lignes, parfois beaucoup plus sur des sites à fort trafic (bots inclus). Première passe de diagnostic (à exécuter en lecture seule) :
SELECT COUNT(*) AS nb_rows FROM ps_configuration;
SELECT
table_rows,
ROUND((data_length+index_length)/1024/1024, 2) AS size_mb
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = 'ps_configuration';
Ensuite, identifiez les clés suspectes : préfixes modules, clés qui contiennent des IDs, ou valeurs sérialisées. Exemple :
SELECT name, LENGTH(value) AS len, date_upd
FROM ps_configuration
WHERE name REGEXP '[0-9]{3,}'
ORDER BY date_upd DESC
LIMIT 50;
Deux requêtes utiles en complément, parce qu’elles donnent rapidement “qui pollue” :
-- Top "préfixes" (approximation) pour repérer un module bavard
SELECT
SUBSTRING_INDEX(name, '_', 2) AS prefix,
COUNT(*) AS nb
FROM ps_configuration
GROUP BY prefix
ORDER BY nb DESC
LIMIT 30;
-- Clés mises à jour récemment (si vous suspectez une écriture en front)
SELECT name, date_upd
FROM ps_configuration
ORDER BY date_upd DESC
LIMIT 50;
Côté production, corrélez les timings avec vos outils. Une montée de latence panier/checkout et une activité MySQL anormale se repèrent vite via APM et monitoring infra. Si vous instrumentez correctement, vous verrez des pics de requêtes sur ps_configuration et des invalidations cache fréquentes. Pour cadrer ça proprement (KPI e-commerce + signaux d’incident), voir : Monitoring PrestaShop : KPI e-commerce, paiements et prévention des erreurs 503 et, côté incidents liés à saturation, Erreur HTTP 503 : diagnostiquer PHP et logs Apache/LiteSpeed.
En multiboutique, le piège est encore plus violent : une clé de configuration peut être définie au niveau global, shop group ou shop. Un module qui “oublie” le scope peut propager un état panier d’un shop à l’autre (mauvaise utilisation de id_shop/id_shop_group). Même en supposant que vous dériviez les clés (ex. STATE_{id_cart}), vous risquez :
- des collisions si vous concaténez mal (
STATE_12vsSTATE_1+2, ou préfixes ambigus) ; - des “restes” après conversion panier → commande (la clé n’est jamais supprimée) ;
- des effets de bord lors d’un import/test (rejeu d’un ID panier, duplication base, etc.).
Et sur le plan SEO/UX, ces bugs se matérialisent par des tunnels incohérents (panier vidé “au hasard”, point relais qui change, total qui “clignote”, étape checkout qui recule) — ce qui s’observe indirectement dans les métriques checkout et l’abandon.
Stocker correctement un état panier : scopes, persistance et contraintes réelles
Avant de coder, posez la question simple : quel scope et quelle durée de vie ? En pratique, on a 4 familles : (1) état strictement front et temporaire (ex. accordéon ouvert) → stockage navigateur ; (2) état par session (ex. étape checkout atteinte) → cookie/session ; (3) état par panier (ex. point relais, métadonnées de livraison, option cadeau) → base, attachée à id_cart ; (4) état durable par client (ex. préférences) → id_customer/table dédiée. Tenter de tout faire passer par ps_configuration est un anti-pattern parce qu’il force un scope global.
Tableau de décision (rapide) :
| Besoin | Exemples | Stockage recommandé | Durée | Risques principaux |
|---|---|---|---|---|
| UI pure | onglet ouvert, filtre local | localStorage/sessionStorage |
minutes/jours | UX (pas critique), pas serveur |
| Session checkout | étape atteinte, flags non critiques | cookie (Context::cookie) |
session | taille (~4KB par cookie, selon navigateur), manipulable |
| État panier | point relais, surcharge, cadeau, métadonnées livraison | table ..._cart_state par id_cart |
heures/jours | nettoyage, concurrence, intégrité |
| Préférence client | “livrer en point relais par défaut”, opt-in | table client / champs dédiés | durable | conformité, migrations |
Pour un état “session-like”, la solution la plus simple dans PrestaShop reste le cookie de contexte : Context::getContext()->cookie. C’est rapide, isolé par navigateur, et ça ne sollicite pas MySQL à chaque action. Limites : taille (les navigateurs imposent des limites, classiquement ~4KB par cookie), exposition côté client (même chiffré/obfusqué, considérez que c’est manipulable), et non partageable cross-device. Pour un état critique (ex. choix point relais qui conditionne le transport), vous voulez généralement une persistance côté serveur.
Pour un état “par panier”, utilisez une table dédiée indexée sur id_cart. C’est la bonne abstraction : le panier est déjà un agrégat persistant (ps_cart, ps_cart_product, etc.). La table dédiée permet d’avoir des contraintes (unicité, TTL, JSON, checksum), et surtout d’éviter d’impacter le chargement global de configuration. À partir de MySQL 5.7+ / 8.0, un champ JSON peut être acceptable si vous restez discipliné :
- évitez de faire des filtres
JSON_EXTRACT()dans des requêtes “chaudes” du checkout ; - préférez une lecture/écriture par clé primaire (
id_cart) ; - si vous devez requêter un attribut précis, envisagez un champ dédié (ou un champ généré indexé).
Pour optimiser vos requêtes, appliquez la même rigueur que pour n’importe quelle table critique (voir : Index MySQL : composer, mesurer avec EXPLAIN et optimiser les requêtes).
Implémentation (module) : remplacer Configuration::updateValue() par une table ps_mymodule_cart_state
Contexte de ce guide : PrestaShop 8.1/8.2/9.1/9.2, PHP 8.2+ (les exemples restent compatibles PHP 8.1). Pré-requis : accès SQL (migration en staging d’abord), sauvegarde, et capacité à déployer un module (ou patch) proprement. Risque principal : casser le checkout si vous migrez à chaud sans fallback ; faites un déploiement progressif avec double-write si nécessaire.
Schéma SQL minimal
Table dédiée avec un enregistrement par panier, et un date_upd pour cleanup.
CREATE TABLE IF NOT EXISTS `ps_mymodule_cart_state` (
`id_cart` INT UNSIGNED NOT NULL,
`id_shop` INT UNSIGNED NOT NULL,
`state` JSON NOT NULL,
`date_add` DATETIME NOT NULL,
`date_upd` DATETIME NOT NULL,
PRIMARY KEY (`id_cart`),
KEY `idx_shop_upd` (`id_shop`, `date_upd`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Pourquoi PRIMARY KEY(id_cart) : accès direct O(1) pour lire/écrire l’état d’un panier. Pourquoi idx_shop_upd : utile pour purger par shop, et pour des stats/diagnostics. Si vous devez gérer plusieurs entrées par panier (ex. plusieurs providers), passez à une clé composite (id_cart, provider) ou ajoutez un UNIQUE.
Deux garde-fous simples à envisager selon votre contexte :
- si votre état contient un identifiant utile au support (ex.
pickup_point_id), ajoutez-le en colonne dédiée pour faciliter le debug (sans parser le JSON) ; - prévoyez un TTL (par exemple purge des états non mis à jour depuis 30 jours), car un panier peut survivre longtemps via relances email, retargeting, ou simple “onglet oublié”.
Accès data côté PHP (sans ObjectModel)
Sur un état panier, vous voulez du code court, testable, et sans magie. Exemple de repository minimal :
final class CartStateRepository
{
public function upsert(int $idCart, int $idShop, array $state): void
{
$json = json_encode($state, JSON_THROW_ON_ERROR);
$now = date('Y-m-d H:i:s');
Db::getInstance()->execute(
'INSERT INTO '._DB_PREFIX_.'mymodule_cart_state (id_cart, id_shop, state, date_add, date_upd)
VALUES ('.(int)$idCart.', '.(int)$idShop.', \''.pSQL($json, true).'\', \''.pSQL($now).'\', \''.pSQL($now).'\')
ON DUPLICATE KEY UPDATE
id_shop = '.(int)$idShop.',
state = \''.pSQL($json, true).'\',
date_upd = \''.pSQL($now).'\''
);
}
public function get(int $idCart): ?array
{
$row = Db::getInstance()->getRow(
'SELECT state FROM '._DB_PREFIX_.'mymodule_cart_state WHERE id_cart='.(int)$idCart
);
if (!$row) {
return null;
}
return json_decode($row['state'], true, 512, JSON_THROW_ON_ERROR);
}
public function delete(int $idCart): void
{
Db::getInstance()->execute(
'DELETE FROM '._DB_PREFIX_.'mymodule_cart_state WHERE id_cart='.(int)$idCart
);
}
}
Points importants : (1) pSQL($json, true) pour autoriser les quotes/UTF-8 ; (2) JSON_THROW_ON_ERROR pour éviter les null silencieux ; (3) le pattern upsert évite un SELECT préalable (moins de round trips, moins de fenêtres de course).
Note compat : VALUES(col) dans ON DUPLICATE KEY UPDATE est encore très répandu, mais certaines versions MySQL le déprécient ; le SQL ci-dessus évite ce point en réinjectant directement les valeurs.
Si vous avez Redis, vous pouvez ajouter un cache read-through en clé cart_state:{id_cart} avec TTL court (ex. 5–15 minutes) après avoir stabilisé le stockage (cf. Cache PrestaShop : erreurs fréquentes et bonnes pratiques multiboutique). Mais évitez de “cacher la poussière sous le tapis” : le problème initial vient de l’anti-pattern d’écriture globale, pas d’un manque de cache.
Migration : détecter, transférer, purger (sans créer un incident prod)
Commencez par localiser où l’état panier est écrit en configuration. Typiquement, vous verrez des patterns comme :
Configuration::updateValue('MYMODULE_CART_STATE', serialize($data))(écrasement global)Configuration::updateValue('MYMODULE_CART_STATE_'.$id_cart, ...)(explosion de clés)
Une fois les clés identifiées, faites une migration en staging : (1) ajouter la table, (2) déployer une version du module qui lit d’abord la table puis fallback configuration, (3) activer une tâche de migration qui copie les données, (4) supprimer l’écriture dans ps_configuration, (5) purger. Si vous devez intervenir via phpMyAdmin, assurez-vous de régler vos accès et protections (voir : phpMyAdmin : réparer l’accès MySQL #1045 et les problèmes CSRF).
Pour rendre la migration robuste, un enchaînement “sans surprise” ressemble à ceci :
- Release 1 (safe) : lecture table → fallback config → écriture dans la table (et éventuellement double-write temporaire si vous ne pouvez pas casser l’existant) ;
- Job de migration : backfill des anciennes entrées (en loggant le volume migré et les erreurs JSON/serialize) ;
- Release 2 : suppression du fallback + suppression de toute écriture config en front ;
- Purge : suppression des clés, puis surveillance.
Pour la purge, faites-le ciblé et loggé. Exemple (à adapter au préfixe réel) :
-- Sauvegarde logique recommandée avant purge.
SELECT COUNT(*) FROM ps_configuration WHERE name LIKE 'MYMODULE_CART_STATE%';
DELETE FROM ps_configuration WHERE name LIKE 'MYMODULE_CART_STATE%';
Sur une grosse table, une suppression massive peut verrouiller longtemps et générer du purge-lag InnoDB. Si vous avez des centaines de milliers de lignes, faites du batch : DELETE ... LIMIT 5000 en boucle, et surveillez Threads_running, innodb_row_lock_time, et le lag réplication si applicable.
Ajoutez aussi une purge côté nouvelle table (sinon vous recréez une “poubelle” ailleurs). Exemple de logique simple :
- supprimer l’état dès qu’une commande est créée (si votre état n’est plus utile après
actionValidateOrder) ; - et/ou purger par ancienneté.
Exemple SQL (à exécuter hors pic) :
-- 1) Suppression des états dont le panier est déjà converti en commande
DELETE s
FROM ps_mymodule_cart_state s
INNER JOIN ps_orders o ON o.id_cart = s.id_cart;
-- 2) Purge des états trop anciens (ex. 30 jours sans update)
DELETE FROM ps_mymodule_cart_state
WHERE date_upd < (NOW() - INTERVAL 30 DAY);
Après purge, vérifiez que votre base est propre et que vous n’avez pas de régressions : le gain n’est pas que “esthétique”, il peut être mesurable sur le temps de réponse et sur la charge DB. Pour une approche de nettoyage base/caches plus large, voir : Optimisation PrestaShop : nettoyer la base et configurer le cache natif.
Garde-fous : empêcher la réintroduction de l’anti-pattern (revue, CI, observabilité)
Le vrai problème n’est pas de migrer une fois ; c’est d’éviter que le pattern revienne via un module tiers, un patch rapide, ou un override legacy. Ajoutez des règles de revue : toute écriture de Configuration::updateValue() dans des hooks front orientés panier/commande (actionCartSave, actionValidateOrder, hooks shipping/payment) doit être justifiée par une donnée réellement “configuration”. Si c’est un état runtime, refusez.
Checklist de revue (rapide, efficace) :
- Est-ce que la donnée est identique pour tous les utilisateurs d’un shop (ou shop group) ? Si non → pas
ps_configuration. - Est-ce que la donnée doit expirer ? Si oui → pas
ps_configuration. - Est-ce que l’écriture arrive sur une route front (panier/checkout/ajax) ? Si oui → suspect.
- Est-ce que la clé contient un identifiant (
id_cart,id_customer,id_address) ? Si oui → anti-pattern quasi certain.
Côté CI, vous pouvez automatiser une détection simple : grep sur updateValue( + présence de $cart/id_cart dans le même fichier, ou règles PHPStan custom. Si vous êtes déjà dans un chantier de modernisation, c’est typiquement le genre de dette qui se traque bien en statique (voir : Migration PHP 8.5 : préparer PrestaShop avec PHPStan et PHPUnit). L’objectif est pragmatique : réduire les écritures globales et les effets de bord cross-user.
Enfin, observez. Une métrique simple et actionnable : taux d’écriture sur ps_configuration (QPS) + top queries + slow query log. Si vous voyez UPDATE ps_configuration dans les pages panier/checkout, c’est déjà un signal. En complément, lors d’un diagnostic “verrous / contention”, pensez à capturer :
SHOW ENGINE INNODB STATUS(ponctuellement, pour voir verrous/deadlocks) ;- le digest des requêtes (via APM ou outils MySQL) ;
- les temps de réponse p95/p99 des endpoints checkout (pas uniquement la moyenne).
Et si votre site est sujet aux pics (TV, campagnes), ce genre d’anti-pattern casse en premier sous charge parce qu’il multiplie les invalidations et les locks (voir : PrestaShop : préparer un pic de trafic avant un passage TV). Pour paraphraser le principe du Twelve-Factor App, « Store config in the environment » (12factor: Store config in the environment) : la configuration n’est pas un endroit où stocker l’état d’exécution. En PrestaShop, ce principe se traduit très concrètement : ps_configuration doit rester un registre de paramètres, pas un substitut à un stockage par panier.
