WordPress
30 septembre 2026

PrestaShop 9.2 : les nouveautés lues par une équipe qui monte déjà un socle dessus

PrestaShop 9.2.0 est sorti en version stable ce 30 septembre. En bref : le SMTP en TLS sur le port 587 fonctionne enfin, la grille des commandes ne trie plus toute la table, deux écrans de configuration passent en multiboutique, et le One Page Checkout est livré dans l’archive mais désactivé. On monte un socle 9.2 multiboutique pour un client depuis la 9.2.0-beta.1, et les pièges qui nous ont coûté du temps ne figurent dans aucune note de version : aucun n’a produit un message qui désigne sa cause. Cet article donne les deux, avec le code du core à chaque fois.

Une précaution de lecture : l’article a été écrit sur la 9.2.0-rc.1, puis revérifié le jour de la sortie sur le tag 9.2.0. La note de version GitHub de la 9.2.0 ne liste que les cinq changements postérieurs à la RC1 (dont le passage à hummingbird v2.1.2) ; le changelog complet, où figurent toutes les PR citées ici, est dans docs/CHANGELOG.txt. Aucun des fichiers du core cités n’a changé entre la RC1 et la version finale.

Les correctifs de PrestaShop 9.2 qui débloquent des boutiques en production

L’envoi SMTP sur le port 587 fonctionne enfin (#42197). L’option « TLS » du back-office forçait le troisième argument d’EsmtpTransport à true, donc du TLS implicite, qui ne répond que sur 465. Le port 587 attend du STARTTLS : il ne pouvait pas fonctionner, et on contournait en basculant sur 465. En 9.2, classes/Mail.php passe par une méthode useImplicitTls() et, quand le chiffrement est activé, transmet null au transport, qui choisit selon le port :

// classes/Mail.php, PrestaShop 9.2.0
$useImplicitTls = self::useImplicitTls($configuration['PS_MAIL_SMTP_ENCRYPTION'] ?? 'off');
$esmtpTransportParameter = $useImplicitTls ? null : false;

Une boutique passée sur 465 continue de fonctionner. Le cas n’a rien de théorique : on l’a croisé récemment chez un client, bloqué en SMTP sur le 587.

La grille des commandes ne trie plus toute la table (#41932). OrderQueryBuilder calculait toutes les colonnes, dont une sous-requête corrélée « nouveau client », pour chaque commande avant le LIMIT. La 9.2 sélectionne d’abord la page d’identifiants, puis n’hydrate que ces lignes. Méfiez-vous des chiffres qui circulent : la PR annonçait environ 5,9 s avant et 130 ms après sur 60 000 commandes, mais son auteur a remesuré le 6 septembre : environ 0,23 s avant, 0,13 s après à chaud. Le gain est réel (environ 44 %), mais d’un ordre de grandeur plus modeste. L’issue d’origine (#41916, environ 25 s sur 61 700 commandes) est toujours ouverte. Pour mesurer votre propre grille avant et après, la méthode est dans notre diagnostic d’un back-office PrestaShop lent.

Deux écrans de configuration gèrent enfin le multiboutique (#41409). « Paramètres avancés > Administration » et « Paramètres de la boutique > Produits » reçoivent les cases à cocher multiboutique, sur le modèle de l’écran « Commandes ».

La facture jointe à l’email de confirmation respecte le réglage de l’état de commande (#42374). Dans PaymentModule::validateOrder(), la condition testait le champ invoice de l’état au lieu de pdf_invoice : la facture partait même case « Joindre la facture PDF » décochée.

Les nouvelles pages CMS ne naissent plus en noindex (#42350). Quand la colonne indexation de ps_cms vaut 0, CmsController assigne nobots au template (ligne 113 de controllers/front/CmsController.php en 9.2.0) et pose $page['meta']['robots'] = 'noindex' (ligne 203). Le formulaire l’initialisait à « non » : une page publiée sans basculer l’interrupteur restait invisible pour Google, sans message. En 9.2, l’indexation est active par défaut ; les pages existantes ne changent pas (requête plus bas).

Des dépréciations PHP 8.5 corrigées dans le panier (#42439). Cart.php et CartRule.php utilisaient des identifiants potentiellement null comme clé de tableau, ce que PHP 8.5 signale par Using null as an array offset is deprecated. Chez nous, ce genre d’avertissement a eu un effet bien plus visible qu’une ligne de log (plus bas).

Côté technique, l’Admin API filtre ses endpoints par version du core (minVersion, maxVersion, #42145). Un champ client personnalisé qui disparaît de l’API après passage en 9.2 fait l’objet d’un article dédié au champ client qui disparaît du webservice.

Le One Page Checkout est-il actif dans PrestaShop 9.2 ? Livré, mais désactivé

Le 20 mai, on présentait le One Page Checkout natif que prépare PrestaShop 9.2. Voici ce qu’on trouve réellement dans le tag 9.2.0.

Le composer.json du core embarque le module :

"prestashop/ps_onepagecheckout": "0.6.7",

L’installeur installe tous les modules présents dans modules/ (getModulesOnDisk()), donc celui-ci aussi. Mais son install() écrit la configuration à zéro :

// ps_onepagecheckout.php, v0.6.7
public const CONFIG_ONE_PAGE_CHECKOUT_ENABLED = 'PS_ONE_PAGE_CHECKOUT_ENABLED';

protected function installOnePageCheckoutConfiguration(): bool
{
    return Configuration::updateValue(self::CONFIG_ONE_PAGE_CHECKOUT_ENABLED, 0, false);
}

Le tunnel en plusieurs étapes reste donc celui par défaut. Le module exige au minimum 9.2.0 dans ps_versions_compliancy, et son README porte toujours, en 0.6.7, l’avertissement « not production-ready ». Le feature flag du core a été retiré avant la bêta (#41006) : tout passe par le module. Même comportement sur notre socle monté depuis la bêta : la distribution 9.2.0-beta.1 embarquait la 0.6.2 du module, avec le même updateValue(..., 0, false) à l’installation.

On l’a activé et testé avec Payplug, sur une boutique client et sur notre propre boutique : le tunnel a fonctionné sans souci. Cela ne lève pas l’avertissement officiel, qui reste à connaître avant de se lancer. Avant de l’activer, les modules de paiement et de transport sont à tester un par un.

Monter un socle PrestaShop 9.2 : les pannes que le changelog ne mentionne pas

Le contexte : un socle PrestaShop 9.2 pour une migration, deux boutiques en multiboutique, un thème enfant de hummingbird, PrettyBlocks pour les contenus. En local, Docker avec nginx, PHP 8.5-fpm, MySQL 8.0 et un conteneur de capture des mails. Pour la suite, la CI est déjà écrite pour construire deux images (php-fpm 8.5 qui tourne sans root, nginx non privilégié) sur les branches de préproduction et de production, destinées à un déploiement Kubernetes piloté en GitOps. Aucun des problèmes ci-dessous n’a produit de message qui pointe vers sa cause.

Partir d’une archive buildée, pas d’un clone du dépôt

Notre premier essai partait d’un clone du dépôt officiel : des assets manquants partout (hummingbird, notre thème enfant, themes/core.js), parce qu’un clone n’est pas une distribution et qu’il faut tout reconstruire.

On est reparti d’une distribution communautaire de la 9.2.0-beta.1, déjà buildée : vendor/, assets hummingbird et themes/core.js inclus, donc ni composer install ni build des thèmes du core à refaire. Elle a un second avantage : elle est construite sans les modules de services de l’éditeur (ps_accounts, ps_eventbus, ps_mbo), que le dépôt officiel inclut. On a vérifié leur absence après installation. Reste notre thème enfant, à construire (npm ci && npm run build) avant son activation, sinon prestashop:theme:enable échoue sur un assets/css/theme.css manquant.

Un détail qui trompe : cette bêta se déclare déjà en 9.2.0 (public const VERSION = '9.2.0'; dans src/Core/Version.php). _PS_VERSION_ ne permet donc pas de distinguer une bêta d’une stable : pour savoir ce qui tourne, il faut remonter à l’archive d’origine.

Une configuration pilotée par l’environnement

L’installeur écrit les accès base de données et les clés dans app/config/parameters.php. En local, ce fichier lit les accès base dans l’environnement que fournit compose.yaml. Dans l’image Docker, il est carrément remplacé par un gabarit où chaque valeur vient de l’environnement, et où une variable obligatoire absente fait échouer le démarrage au lieu de retomber sur une valeur par défaut. Version raccourcie :

<?php
// app/config/parameters.php dans l'image (raccourci)
$env = static function (string $name, ?string $default = null): string {
    $value = getenv($name);
    if ($value === false || $value === '') {
        if ($default === null) {
            throw new RuntimeException(sprintf('Missing required environment variable "%s".', $name));
        }
        return $default;
    }
    return $value;
};

return [
    'parameters' => [
        'database_host' => $env('DB_HOST'),
        'database_name' => $env('DB_NAME'),
        'database_user' => $env('DB_USER'),
        'database_password' => $env('DB_PASSWORD'),
        'database_prefix' => $env('DB_PREFIX', 'ps_'),
        'mailer_host' => $env('MAILER_HOST', '127.0.0.1'),
        'secret' => $env('PS_APP_SECRET'),
        'cookie_key' => $env('PS_COOKIE_KEY'),
        // ...
    ],
];

Le parameters.php de développement est exclu par .dockerignore : il n’atteint jamais une image.

Un point à savoir sur mailer_host : la clé existe dans les paramètres, mais classes/Mail.php ne la lit pas. L’envoi SMTP de PrestaShop prend son serveur dans PS_MAIL_SERVER, en table configuration. Piloter le mailer par l’environnement demande donc aussi de régler cette valeur.

La cookie_key passe elle aussi par l’environnement, en local comme dans l’image, et migration oblige, ce n’est pas celle de l’installation. Les mots de passe clients migrés depuis PrestaShop 1.6 sont des md5(_COOKIE_KEY_ . password) : PrestaShop 9 ne les reconnaît (puis ne les réécrit en bcrypt à la première connexion) qu’avec la clé d’origine. Deux anciennes boutiques fusionnent dans ce socle et une seule clé est possible : on a gardé celle de la boutique qui a la plus grosse base clients. En local, sans la variable, c’est la clé générée à l’installation qui s’applique ; dans l’image, son absence bloque le démarrage. Les conséquences sont dans l’article sur la cookie key et la migration des secrets.

Le front qui ne réagit plus, sans une seule erreur serveur

Le symptôme : la page s’affiche, les styles sont là, mais plus aucun comportement JavaScript du front ne répond, sans aucune erreur côté serveur. Il nous a fallu une demi-journée pour en trouver la cause.

FrontController construit l’objet JS prestashop avec Media::addJsDef() (ligne 595 de classes/controller/FrontController.php en 9.2.0) et le passe au thème dans js_custom_vars (ligne 753). Le head.tpl les rend via _partials/javascript.tpl, qui les imprime en ligne :

{* templates/_partials/javascript.tpl, identique dans classic 3.1.0 et 3.1.2, hummingbird v2.1.0 et v2.1.2 *}
{if isset($vars) && $vars|@count}
  <script type="text/javascript">
    {foreach from=$vars key=var_name item=var_value}
    var {$var_name} = {$var_value|json_encode nofilter};
    {/foreach}
  </script>
{/if}

Notre thème enfant hérite de ce partial de hummingbird. Avec le mode debug actif et PHP 8.5, des avertissements E_DEPRECATED venus du core (dont Cart.php et son offset null sur un tableau, celui que corrige #42439) s’imprimaient dans la sortie HTML au milieu de ce bloc, en plein var prestashop = …. Le navigateur reçoit un script syntaxiquement invalide et abandonne tout le bloc : prestashop n’est jamais défini, jQuery n’est jamais exposé, et tout le JavaScript du front tombe en cascade. La trace est dans la console du navigateur, pas côté serveur.

Le réflexe de diagnostic : afficher le source brut de la page (pas l’inspecteur) et chercher Deprecated ou Warning à l’intérieur des balises <script>.

Pour le correctif, un display_errors = Off dans le php.ini du conteneur ne suffit pas : config/defines.inc.php le réécrit à chaque requête avec ini_set(), à on quand _PS_MODE_DEV_ vaut true. On voulait garder le mode debug en local : on a posé _PS_DISPLAY_COMPATIBILITY_WARNING_ à false, qui retire E_DEPRECATED et E_USER_DEPRECATED du niveau d’erreur. Le tout vit dans config/defines_custom.inc.php, que config/config.inc.php charge avant defines.inc.php. Le mode debug lui-même y est piloté par l’environnement, pour qu’une préprod ou une production ne l’active jamais par accident :

<?php
// config/defines_custom.inc.php (commentaires retirés)
define('_PS_DISPLAY_COMPATIBILITY_WARNING_', false);

if (!defined('_PS_MODE_DEV_') && getenv('PS_DEV_MODE') !== false) {
    define('_PS_MODE_DEV_', getenv('PS_DEV_MODE') === '1');
}

Le compose.yaml local passe PS_DEV_MODE: "1", les autres environnements 0. Aucune ligne du core n’est modifiée : defines.inc.php ne définit ces constantes que si elles ne le sont pas déjà.

Pour mémoire, l’installeur de la 9.2.0 accepte PHP de 8.1 à 8.5 (_PS_INSTALL_MAXIMUM_PHP_VERSION_ vaut '8.5' dans install-dev/install_version.php). Sur la bêta, on a aussi coupé le CCC en local (PS_CSS_THEME_CACHE et PS_JS_THEME_CACHE à 0) : certains fichiers assets/cache/bottom-*.js n’étaient jamais écrits, la requête tombait en 404 et le navigateur recevait une page d’erreur HTML à la place du JavaScript.

La deuxième boutique qui renvoie vers la première

Le symptôme : la deuxième boutique apparaît dans le back-office, mais son domaine affiche la première, sans page d’erreur.

Notre script de provisionnement avait créé la boutique en copiant la structure de la première (sans les produits) avec Shop::copyShopData(). En 9.2, cette méthode copie les tables *_shop et *_lang de Shop::getAssoTables(), mais ne touche jamais à ps_shop_url : aucun domaine ne pointe vers la nouvelle boutique.

La suite est dans Shop::initialize(). Le domaine demandé est cherché dans ps_shop_url par findShopByHost(). Aucun résultat, donc aucun id_shop :

// classes/shop/Shop.php, Shop::initialize(), PrestaShop 9.2.0
$shop = new Shop($id_shop);
if (!Validate::isLoadedObject($shop) || !$shop->active) {
    // No shop found ... too bad, let's redirect to default shop
    $default_shop = new Shop((int) Configuration::get('PS_SHOP_DEFAULT'));
    // ...
    $redirect_type = Configuration::get('PS_CANONICAL_REDIRECT');
    $redirect_code = ($redirect_type == 1 ? '302' : '301');
    // ...
}

Redirection vers la boutique par défaut (302 par défaut), sans aucun log. Le diagnostic tient en une requête :

SELECT s.id_shop, s.name, su.domain, su.domain_ssl, su.physical_uri, su.main
FROM ps_shop s
LEFT JOIN ps_shop_url su ON su.id_shop = s.id_shop;

Une boutique avec domain à NULL est une boutique qui redirige. Le script qui posait ensuite les domaines ne pouvait pas le voir : il faisait un UPDATE ... WHERE id_shop = X AND main = 1, qui touchait zéro ligne sans la moindre erreur SQL, et affichait quand même la boutique comme configurée. On l’a corrigé pour qu’il vérifie la présence de la ligne et la crée quand elle manque. Notre script le fait en SQL direct ; avec la classe du core, cela donne :

// Exemple réécrit : à exécuter après Shop::copyShopData()
$shopUrl = new ShopUrl();
$shopUrl->id_shop = (int) $newShop->id;
$shopUrl->domain = $domain;
$shopUrl->domain_ssl = $domain;
$shopUrl->physical_uri = '/';
$shopUrl->virtual_uri = '';
$shopUrl->main = true;
$shopUrl->active = true;
$shopUrl->add();

Depuis, les deux boutiques répondent 200, chacune avec son propre contexte. Autre surprise multiboutique : des modules installés qui ne s’activent que sur la boutique par défaut.

Un cache Symfony qui fait croire à une mauvaise base de données

Après une installation ratée, les suivantes partaient se connecter sur localhost en root, malgré les bons accès dans l’environnement : le conteneur relisait le cache Symfony compilé pendant l’échec. Le réflexe : après toute installation qui a échoué, vider var/cache/ avant de remettre en cause la configuration.

rm -rf var/cache/*

Une boutique à moitié en anglais

Dernier piège, vu sur un autre projet 9.2 en préproduction : « Sort by », « Relevance », « Home » au milieu d’une boutique française, alors que tout était en français en local. Le dossier translations/ de l’image ne contenait que cldr et default : une règle translations/*-*/ du .gitignore écartait le pack fr-FR (169 catalogues XLIFF, 3,1 Mo). Seules les 53 chaînes posées en base par nos scripts sortaient en français. Le correctif : versionner le pack via une exception dans le .gitignore, et le re-télécharger à chaque montée de version, car il est figé sur celle-ci. L’enquête complète : quand le .gitignore livre PrestaShop 9.2 en anglais.

Le réflexe à retenir

Aucune de ces pannes n’a laissé d’erreur serveur qui mène à la cause. Trois contrôles les auraient repérées plus tôt :
1. le source brut de la page (pas l’inspecteur) pour repérer un Deprecated imprimé dans un <script> ;
2. la jointure ps_shop / ps_shop_url pour vérifier que chaque boutique a son domaine ;
3. le contenu réel de ce qui tourne (image livrée, var/cache/) plutôt que celui du dépôt.

Comment vérifier si votre boutique est concernée par ces correctifs 9.2

Deux requêtes, à lancer avant la mise à jour (préfixe ps_ à adapter).

Les pages CMS publiées mais exclues de l’index, qui ne changeront pas toutes seules :

SELECT c.id_cms, cl.id_shop, cl.meta_title, c.active, c.indexation
FROM ps_cms c
JOIN ps_cms_lang cl ON cl.id_cms = c.id_cms
WHERE c.active = 1 AND c.indexation = 0;

Le réglage d’envoi des emails par boutique (PS_MAIL_METHOD à 2 = SMTP). Une boutique sur 465 a peut-être été déplacée là pour contourner le bug du 587 :

SELECT id_shop_group, id_shop, name, value
FROM ps_configuration
WHERE name IN ('PS_MAIL_METHOD', 'PS_MAIL_SMTP_ENCRYPTION', 'PS_MAIL_SMTP_PORT');

Ce qu’on a revérifié le jour de la sortie

Cet article a été écrit sur la RC1. Le 30 septembre, on a relu le tag 9.2.0 :

  • les PR citées (#42197, #41932, #41409, #42374, #42350, #42439, #42145, et #41006 pour le feature flag) figurent toutes dans le docs/CHANGELOG.txt de la 9.2.0, sans revert entre la RC1 et la finale ;
  • prestashop/ps_onepagecheckout reste épinglé en 0.6.7 dans le composer.json, et son README porte toujours l’avertissement « not production-ready » ;
  • _PS_INSTALL_MAXIMUM_PHP_VERSION_ vaut toujours '8.5' ;
  • les thèmes livrés passent en classic 3.1.2 et hummingbird v2.1.2, avec le même _partials/javascript.tpl ;
  • le pack fr-FR de la 9.2.0 est téléchargeable (https://i18n.prestashop-project.org/translations/9.2.0/fr-FR/fr-FR.zip).

L’issue #41916 sur la lenteur de la grille des commandes est, elle, toujours ouverte.

PrestaShop 9.2 en résumé : des correctifs utiles, des risques dans l’environnement

PrestaShop 9.2 consolide plus qu’il ne bouleverse, et un One Page Checkout arrive éteint. Les vrais risques sont ailleurs : PHP 8.5 avec les erreurs affichées, le multiboutique créé par script, le contenu réel de l’image livrée. Si vous vous demandez plutôt s’il faut migrer maintenant et depuis quelle version, c’est l’objet de notre article sur la migration de PrestaShop 1.7 vers 8 et l’anticipation de PrestaShop 9.

Nous vous recommandons aussi