PrestaShop modules : préfixer les clés Configuration sans supprimer l’existant

Guide pratique pour préfixer les clés Configuration dans PrestaShop sans casser la compatibilité : duplication SQL/PHP idempotente, fallback multiboutique, métriques et sécurité.

Ordinateurs montrant du code et des schémas techniques pour la configuration de clés.

Table des matières :

  1. Préfixer les clés Configuration : réduire les collisions et rendre un module maintenable
  2. Ce que vous migrez exactement : table ps_configuration, multiboutique et cache statique
  3. Stratégie “sans casse” : dupliquer, synchroniser, déprécier (mais ne pas supprimer)
  4. Implémentation module : scripts d’upgrade idempotents + migration SQL (PrestaShop 8.2/9.2, PHP 8.2)
  5. Lecture/écriture dans le module : fallback propre + dépréciation mesurable
  6. Sécurité et exploitation : secrets, rollback, CI

Préfixer les clés Configuration : réduire les collisions et rendre un module maintenable

Dans le cœur PrestaShop, la classe Configuration fournit un simple KV-store persistant en base (table ps_configuration). Il n’y a aucun mécanisme de namespacing imposé : si deux modules utilisent la même clé (API_KEY, ENABLED, DEBUG, etc.), ils écrivent dans le même espace global. En pratique, ce n’est pas un cas d’école : sur des boutiques historiques, on tombe régulièrement sur des clés génériques posées par des modules legacy, puis réutilisées plus tard par un autre intégrateur.

Un scénario typique (et coûteux) : une boutique en multistore “par pays” (FR/BE/CH) ou “par enseigne/franchise” cumule des modules historiques. Un nouveau module arrive, lit DEBUG “au hasard” (valeur laissée par un ancien module), active un mode verbeux en production, et vous vous retrouvez avec des logs saturés / des appels API superflus. Le bug est d’autant plus traître que la clé existe : rien ne crashe, mais le comportement est faux.

Préfixer les clés Configuration avec l’identifiant du module (ex. MYMODULE_API_KEY) est la seule protection fiable contre ces collisions. C’est aussi un prérequis pour faire évoluer un module sans “effet de bord” sur l’existant : quand vous introduisez une nouvelle option ENABLED, vous ne voulez pas écraser un ENABLED déjà exploité par un autre module, ni lire une valeur qui ne vous appartient pas.

Quelques règles pratiques qui évitent les surprises à long terme :

  • Préfixe stable et unique : le plus simple est strtoupper($this->name) (nom technique du module) + _.
  • Évitez les noms “sémantiques” trop courts (PAY_, API_, SYNC_) : ils redeviennent génériques à grande échelle.
  • Gardez un mapping clair des anciennes → nouvelles clés (utile en audit et en support).
  • Anticipez la multiboutique dès le design : une clé non préfixée et non contextualisée, c’est le combo parfait pour des comportements différents entre BO/FO ou entre boutiques.

Le piège, c’est la migration : si votre module est déjà installé chez des clients avec des clés non préfixées, “renommer” brutalement en supprimant l’ancien (Configuration::deleteByName('API_KEY')) casse la rétrocompatibilité, empêche les rollbacks (downgrade), et peut casser des overrides/templates qui lisaient encore l’ancienne clé. Le but ici est donc précis : préfixer les clés sans supprimer l’existant, en restant idempotent et multiboutique.


Ce que vous migrez exactement : table ps_configuration, multiboutique et cache statique

Sur PrestaShop 1.7.8+ jusqu’à 9.x, Configuration persiste dans ps_configuration (préfixe variable), typiquement avec les colonnes : id_configuration, id_shop_group, id_shop, name, value, date_add, date_upd. Le même name peut exister en plusieurs lignes selon le contexte multiboutique (global, groupe, boutique). En multistore, une migration “à plat” qui oublie id_shop crée des incohérences difficiles à diagnostiquer (valeur globale qui écrase une valeur boutique, ou l’inverse).

Deux points sont souvent sous-estimés :

  • Le niveau “groupe de boutiques” (id_shop_group) existe réellement : certaines configurations peuvent être définies “au groupe” et se répercuter sur plusieurs boutiques. Si vous ne migrez que global + boutique, vous ratez un cas d’usage (et vous créez des écarts “inexpliqués” pour les marchands).
  • Les clés multilingues : certaines configurations sont stockées via ps_configuration_lang (selon usage de Configuration::updateValue() avec true/array de langues). Si votre clé legacy est multilang, copier uniquement ps_configuration revient à perdre les valeurs traduites.

Requête utile pour repérer si une clé a une déclinaison multilang (présence en ps_configuration_lang) :

SELECT c.name, COUNT(cl.id_lang) AS nb_lang
FROM ps_configuration c
JOIN ps_configuration_lang cl ON cl.id_configuration = c.id_configuration
WHERE c.name IN ('API_KEY', 'MYMODULE_API_KEY')
GROUP BY c.name;

À garder en tête : l’accès via Configuration::get() s’appuie sur un cache statique (en mémoire PHP) pour limiter les requêtes. Si vous migrez par SQL direct, vous devez forcer une invalidation (au minimum, Configuration::loadConfiguration()/clearConfigurationCache() selon versions), sinon vous lisez encore les anciennes valeurs dans le même process (typiquement en back-office lors d’un upgrade déclenché via module manager).

Pour vérifier votre état avant/après migration, faites des requêtes explicites. Exemple (adaptez le préfixe de table) :

SELECT id_shop_group, id_shop, name, value
FROM ps_configuration
WHERE name IN ('API_KEY', 'MYMODULE_API_KEY')
ORDER BY id_shop_group, id_shop;

Si vous voyez plusieurs lignes pour API_KEY avec des id_shop différents, une migration “copie globale” est une régression. Votre logique doit donc dupliquer les valeurs par contexte.

Astuce d’audit rapide pour détecter les clés “à risque” (génériques non préfixées), avant même d’écrire une migration : listez les noms courts les plus fréquents dans la base (sur des boutiques anciennes, on voit souvent DEBUG, TOKEN, KEY, MODE, ENABLED, etc.) :

SELECT name, COUNT(*) AS nb
FROM ps_configuration
GROUP BY name
HAVING nb > 1
ORDER BY nb DESC, name ASC;


Stratégie “sans casse” : dupliquer, synchroniser, déprécier (mais ne pas supprimer)

La stratégie la plus robuste n’est pas un renommage (UPDATE du name), mais une duplication contrôlée : 1) vous créez les nouvelles clés préfixées si elles n’existent pas, en copiant la valeur des anciennes clés ; 2) vous adaptez le code du module pour lire d’abord la nouvelle clé, et retomber sur l’ancienne si nécessaire (fallback) ; 3) vous pouvez, optionnellement, maintenir une synchronisation (écrire dans les deux) pendant une ou deux versions, puis ne plus écrire dans l’ancienne.

Cette approche supporte les scénarios réels : rollback vers une version antérieure, environnement de staging qui garde une base “sale”, ou coexistence de jobs externes qui lisent encore API_KEY. Elle est aussi plus sûre en multiboutique : au lieu de risquer un conflit sur un index unique (name, id_shop_group, id_shop), vous insérez une nouvelle clé sans toucher à l’ancienne.

Pour garder la migration maîtrisée, formalisez un mini “contrat” de migration sous forme de tableau (utile pour la QA et le support) :

Fonctionnel Clé legacy Nouvelle clé (préfixée) Source de vérité après migration Durée de fallback
Auth API API_KEY MYMODULE_API_KEY MYMODULE_API_KEY 1–2 versions
Activation ENABLED MYMODULE_ENABLED MYMODULE_ENABLED 1 version
Debug DEBUG MYMODULE_DEBUG MYMODULE_DEBUG 0–1 version

Côté versioning, évitez les migrations “silencieuses” hors SemVer : vous modifiez un contrat de configuration, donc vous devez livrer ça dans une release claire et testée. Le rappel SemVer est utile ici : « Once a versioned package has been released, the contents of that version MUST NOT be modified. » (Semantic Versioning 2.0.0, https://semver.org/). Concrètement : migration dans un script d’upgrade dédié, pas dans un commit “hotfix” non versionné.

Checklist pragmatique (qui évite 80% des retours “ça marche chez moi”) :

  • [ ] migration idempotente (exécution 2 fois = même état final)
  • [ ] migration par contexte (global / groupe / boutique, selon votre usage)
  • [ ] fallback lecture temporaire avec métrique (log/warning)
  • [ ] synchro écriture (optionnelle) limitée dans le temps
  • [ ] plan de rollback explicite (downgrade tolérable) + note de release


Implémentation module : scripts d’upgrade idempotents + migration SQL (PrestaShop 8.2/9.2, PHP 8.2)

Les exemples ci-dessous sont pensés pour PrestaShop 8.2.x et 9.2.x en PHP 8.2 (mêmes concepts en 1.7.8, avec quelques différences mineures). Le point clé : mettre la migration dans upgrade/upgrade-X.Y.Z.php et l’appeler depuis Module::install()/Module::enable() si vous devez aussi couvrir des installations “repackagées”. Pour cadrer la livraison (sans override, avec tests), vous pouvez réutiliser la méthodo de l’article : Module PrestaShop sans override : méthode de cadrage, tests et livraison du code.

Un squelette minimal de script d’upgrade (utile pour situer le “où”) :

<?php
// upgrade/upgrade-1.4.0.php

if (!defined('_PS_VERSION_')) {
    exit;
}

function upgrade_module_1_4_0($module): bool
{
    // Exemple: migration d'une clé legacy vers clé préfixée
    return migrateConfigKeyWithPrefix('API_KEY', 'MYMODULE_API_KEY');
}

1) Helper de migration en PHP (lisible, mais peut générer beaucoup de requêtes)

Cas simple : quelques clés, peu de boutiques. On migre en bouclant sur les contextes shop.

<?php
use Shop;
use Configuration;

/**
 * Copie une clé legacy vers une clé préfixée, par contexte multiboutique.
 * - Ne supprime rien.
 * - Idempotent : ne réécrit pas si la clé cible existe déjà.
 */
function migrateConfigKeyWithPrefix(string $legacyKey, string $prefixedKey): bool
{
    $ok = true;

    // 1) Global (id_shop=0, id_shop_group=0)
    if (!Configuration::hasKey($prefixedKey) && Configuration::hasKey($legacyKey)) {
        $ok = $ok && Configuration::updateGlobalValue($prefixedKey, Configuration::get($legacyKey));
    }

    // 2) Par groupe de boutiques (si multistore)
    // Selon les versions, vous pouvez avoir besoin d'utiliser getShopGroups()/getGroups().
    if (Shop::isFeatureActive() && method_exists(Shop::class, 'getShopGroups')) {
        foreach (Shop::getShopGroups() as $group) {
            $idShopGroup = (int) $group['id_shop_group'];

            // Attention: selon versions, la signature de hasKey/get varie.
            // L'idée: copier la valeur contextualisée au groupe si elle existe.
            if (!Configuration::hasKey($prefixedKey, null, $idShopGroup) && Configuration::hasKey($legacyKey, null, $idShopGroup)) {
                $value = Configuration::get($legacyKey, null, $idShopGroup);
                $ok = $ok && Configuration::updateValue($prefixedKey, $value, false, $idShopGroup);
            }
        }
    }

    // 3) Par boutique (si multistore)
    if (Shop::isFeatureActive()) {
        foreach (Shop::getShops(false, null, true) as $idShop) {
            // NB: hasKey/get acceptent (idShopGroup, idShop) selon versions.
            if (!Configuration::hasKey($prefixedKey, null, (int)$idShop) && Configuration::hasKey($legacyKey, null, (int)$idShop)) {
                $value = Configuration::get($legacyKey, null, null, (int)$idShop);
                $ok = $ok && Configuration::updateValue($prefixedKey, $value, false, null, (int)$idShop);
            }
        }
    }

    // Important si vous mixez SQL direct + Configuration::get() dans le même process.
    if (method_exists(Configuration::class, 'clearConfigurationCache')) {
        Configuration::clearConfigurationCache();
    } else {
        // Sur certaines versions, recharger la conf évite de relire l'ancien état
        if (method_exists(Configuration::class, 'loadConfiguration')) {
            Configuration::loadConfiguration();
        }
    }

    return $ok;
}

En volumétrie, ce pattern peut devenir coûteux : pour N clés × S shops, vous faites en gros O(N×S) lectures/écritures (donc des centaines à milliers de requêtes si S est élevé). Sur un multistore 30 boutiques avec 25 clés legacy, ça fait 750 écritures (et presque autant de lectures). Dans ce cas, basculez sur une migration SQL.

Point d’attention supplémentaire : si la valeur legacy est vide ('') ou null, décidez explicitement si vous migrez quand même. En pratique, beaucoup de boutiques ont des clés “présentes mais inutilisées”. Une stratégie simple est :

  • si la clé legacy n’existe pas → ne rien créer ;
  • si elle existe mais vaut vide → créer uniquement si votre module la considère comme un état significatif (ex. ENABLED=0).

2) Migration SQL en une passe (rapide, mais à manipuler avec précaution)

La migration SQL consiste à insérer les lignes préfixées en copiant value depuis les legacy, sans écraser les nouvelles si elles existent déjà. Avec MySQL/MariaDB, une approche pragmatique est INSERT ... SELECT + LEFT JOIN pour exclure les doublons.

Pré-requis : backup, et exécution via script d’upgrade (pas à la main en prod). Si vous avez des besoins d’optimisation/lecture de plans, l’article Index MySQL : composer, mesurer avec EXPLAIN et optimiser les requêtes sert de base.

-- Exemple : migrer API_KEY -> MYMODULE_API_KEY pour tous les contextes
INSERT INTO ps_configuration (id_shop_group, id_shop, name, value, date_add, date_upd)
SELECT c.id_shop_group,
       c.id_shop,
       'MYMODULE_API_KEY' AS name,
       c.value,
       NOW(),
       NOW()
FROM ps_configuration c
LEFT JOIN ps_configuration target
  ON target.id_shop_group = c.id_shop_group
 AND target.id_shop = c.id_shop
 AND target.name = 'MYMODULE_API_KEY'
WHERE c.name = 'API_KEY'
  AND target.id_configuration IS NULL;

Ce SQL est idempotent : le LEFT JOIN empêche d’insérer si la clé cible existe déjà pour le même contexte. Il ne supprime rien et ne modifie pas les lignes legacy. Ensuite, invalidez le cache config côté PHP si vous enchaînez des lectures dans le même process d’upgrade.

Deux vérifications simples après exécution (pratiques en CI sur une base fixture) :

-- Combien de contextes legacy existent ?
SELECT COUNT(*) FROM ps_configuration WHERE name = 'API_KEY';

-- Combien de contextes préfixés existent après migration ?
SELECT COUNT(*) FROM ps_configuration WHERE name = 'MYMODULE_API_KEY';

Si la seconde requête renvoie moins que la première, ce n’est pas forcément un bug (certaines lignes legacy peuvent être absentes selon contexte), mais c’est un signal : regardez les contextes manquants (id_shop_group, id_shop) et assurez-vous que c’est attendu.


Lecture/écriture dans le module : fallback propre + dépréciation mesurable

Une fois les clés préfixées en base, le vrai travail est dans le code : vous devez vous assurer que la lecture de configuration ne dépend plus uniquement des anciennes clés. Le pattern minimal : getPrefixedOrLegacy() et setBothDuringTransition().

<?php
final class MyModuleConfig
{
    public const PREFIX = 'MYMODULE_';

    public static function key(string $suffix): string
    {
        return self::PREFIX . $suffix;
    }

    public static function get(string $suffix, $default = null)
    {
        $newKey = self::key($suffix);
        if (Configuration::hasKey($newKey)) {
            return Configuration::get($newKey);
        }

        // Fallback legacy (à retirer après dépréciation)
        if (Configuration::hasKey($suffix)) {
            return Configuration::get($suffix);
        }

        return $default;
    }

    /**
     * Pendant 1-2 versions : écrire dans les deux pour éviter les régressions.
     */
    public static function set(string $suffix, $value): bool
    {
        $ok = Configuration::updateValue(self::key($suffix), $value);
        // Legacy sync (optionnel)
        $ok = $ok && Configuration::updateValue($suffix, $value);
        return $ok;
    }
}

Ce fallback a un avantage concret : vous pouvez déployer la nouvelle version du module sur un parc hétérogène sans imposer une migration “parfaite” à 100%. En revanche, ne laissez pas ce code en place indéfiniment : il entretient une ambiguïté (deux sources de vérité).

Pour rendre la dépréciation mesurable, vous pouvez ajouter une alerte uniquement quand le legacy est réellement utilisé. Exemple d’approche “low-noise” :

  • si la clé préfixée est absente et la legacy est présente → loggez un warning (une fois par process ou avec rate limit) ;
  • incluez la version du module dans le message, pour savoir “combien de temps” la compat traîne.

Même sans stack Monolog avancée, une écriture dans les logs PHP ou dans ps_log peut suffire — l’objectif est d’avoir un indicateur fiable pour décider quand retirer le fallback.

Enfin, évitez les clés “dynamiques” du style MYMODULE_SETTING_{$id} qui explosent ps_configuration (table déjà très sollicitée). Si vous avez une configuration par entité (transporteur, pays, produit…), envisagez une table dédiée avec une vraie clé primaire, et des indexes adaptés. Pour objectiver ce genre de décision (impact requêtes/CPU/IO), appuyez-vous sur une démarche d’audit : Audit performance PrestaShop : méthode reproductible et preuves terrain.


Sécurité et exploitation : secrets, rollback, CI

Les clés Configuration finissent souvent par stocker des secrets (API keys, tokens, webhooks). C’est pratique, mais le cœur PrestaShop n’offre pas un coffre-fort : c’est une valeur en clair en base, lisible par toute personne ayant accès SQL (et parfois exportée en debug). Le principe “12-factor” reste un rappel utile même en e-commerce legacy : « Store config in the environment. » (The Twelve-Factor App, https://12factor.net/config). Si vous contrôlez l’infra, la meilleure option est souvent : secret en variable d’environnement / vault, et en base uniquement un identifiant de secret.

Si vous n’avez pas le choix et devez stocker un secret en base, chiffrez côté applicatif (libsodium) avec une clé maîtresse hors base (env var, secret manager). Ça ne rend pas tout magique (rotation, sauvegardes chiffrées, gestion des accès), mais ça évite le “tout en clair”. Dans tous les cas :

  • ne loggez jamais la valeur (même en DEBUG) ;
  • sur les écrans BO, masquez la valeur par défaut (champ “mot de passe” + bouton “révéler” si nécessaire) ;
  • pensez aux exports (support, debug, dumps) : ce sont souvent eux qui fuient les secrets.

Sur la partie exploitation, le rollback est précisément la raison pour laquelle vous ne supprimez pas les anciennes clés. La trajectoire la plus sûre ressemble à ceci :

  1. Version N : migration + lecture fallback + (optionnel) écriture double
  2. Version N+1 : lecture fallback toujours, mais on n’écrit plus dans le legacy (on limite les divergences)
  3. Version N+2 : suppression du fallback (et seulement là, si vous avez des métriques qui montrent que plus personne n’en dépend)

Si vous décidez un jour de nettoyer les clés legacy, faites-le dans une étape séparée (et annoncée), idéalement avec un outil de diagnostic préalable (“ces clés sont encore utilisées ?”). Ne mélangez pas “changement de contrat” et “ménage” dans la même release.

Enfin, évitez d’exposer ces valeurs dans les templates, et limitez les surfaces d’injection/clickjacking côté BO : sur les écrans de configuration module, une CSP stricte est un vrai plus, cf. CSP frame-ancestors : empêcher le clickjacking et l’intégration non autorisée.

Dernier point, non négociable : testez la migration. Au minimum :

  • un test d’upgrade sur base de fixture (avec et sans multistore) ;
  • un check que l’upgrade est idempotent (exécutable deux fois sans changement) ;
  • une validation via ps-validator et CI/CD pour éviter de livrer un script upgrade-*.php cassé.

Pour l’industrialisation, l’article ps-validator : intégration CI/CD du Validator PrestaShop en terminal sert de base opérationnelle. Le résultat attendu est simple : vous avez des clés préfixées, sans suppression des legacy, sans régression multiboutique, et une trajectoire claire de dépréciation.


À lire aussi