Vos fiches produits, vos catégories, vos pages CMS : au lendemain d’une migration, les accents y sont devenus des séquences du genre é ou è. Et le premier geste, c’est de partir en quête d’un réglage d’encodage du côté de PrestaShop. Il n’y en a aucun, voilà précisément ce qui brouille le diagnostic.
Impossible d’y toucher, le cœur fixe lui-même ce jeu de caractères pour la connexion, donc je vais chercher d’un autre côté : le dump d’abord, puis l’interclassement de vos tables, puis les octets eux-mêmes (le dump sort gagnant neuf fois sur dix). Et si la base a trop souffert pour qu’on la répare à l’aveuglette, je peux reprendre ce nettoyage.
Ce que PrestaShop impose côté connexion
La fausse piste que je croise le plus souvent, et de très loin, tient en une ligne : il y aurait, dans les fichiers de la boutique, un paramètre de configuration à rectifier. Rien de tel.
Aucun paramètre à régler
Aucune clé de jeu de caractères dans app/config/parameters.php. Depuis la 1.7.8, la classe chargée d’ouvrir la connexion inscrit charset=utf8mb4 en dur dans la chaîne de connexion, puis enchaîne sur un SET NAMES utf8mb4. Sur 1.7.6 et avant, c’était utf8, soit utf8mb3. Cherchez ailleurs.
Ce changement explique bien des migrations ratées
Montez une base en 1.7.6, reprenez-la sous 8.x, et le jeu de caractères de la connexion bascule sans que le moindre avertissement n’apparaisse où que ce soit, ce que je retrouve derrière quantité de migrations ratées. Et les accents ne font pas cavalier seul : la même bascule déclenche assez souvent une erreur 500 au premier chargement, dès qu’un module est resté en arrière. Faites tourner le script plus bas : il affiche ce que votre serveur déclare pour de bon à la boutique, et il s’assure du même coup qu’un accent revient indemne de l’aller-retour.
<?php
/**
* Jeu de caractères réellement négocié entre PrestaShop et MySQL.
* Déposez ce fichier à la racine de la boutique sous le nom diag-charset.php,
* lancez-le en SSH : php diag-charset.php
* Supprimez-le une fois le diagnostic terminé.
*/
require dirname(__FILE__) . '/config/config.inc.php';
$db = Db::getInstance();
echo "--- Variables de session MySQL ---\n";
foreach ($db->executeS("SHOW VARIABLES LIKE 'character_set%'") as $ligne) {
printf("%-26s : %s\n", $ligne['Variable_name'], $ligne['Value']);
}
foreach ($db->executeS("SHOW VARIABLES LIKE 'collation%'") as $ligne) {
printf("%-26s : %s\n", $ligne['Variable_name'], $ligne['Value']);
}
echo "\n--- Aller-retour sur un accent ---\n";
$test = 'éàçœ';
$retour = $db->getValue("SELECT '" . pSQL($test) . "'");
printf("envoyé : %s\n", $test);
printf("relu : %s\n", $retour);
printf("identique : %s\n", $test === $retour ? 'oui' : 'non');
/* Il n'existe aucun paramètre database_charset dans app/config/parameters.php.
Depuis la 1.7.8, la classe DbPDO écrit en dur charset=utf8mb4 dans le DSN
et exécute SET NAMES utf8mb4 à la connexion. Sur les 1.7.6 et antérieures,
c'était utf8 (soit utf8mb3). Le jeu de caractères de la connexion ne se
règle donc pas côté PrestaShop, mais côté serveur et côté données. */
L’export et l’import du fichier SQL
La corruption que je croise prend presque toujours naissance au transfert : un dump mal déclaré fige des octets faux, et aucun réglage appliqué par la suite ne les redressera. C’est dans le fichier.
Forcer le jeu de caractères des deux côtés
Rien d’explicite dans la commande, et voilà le client MySQL qui se cale sur le serveur et retranscode vos données sans prévenir : ajoutez --default-character-set=utf8mb4 des deux côtés, en sortie comme en entrée (y compris si les deux machines paraissent jumelles). Pour ma part, je le passe toujours. Jumelles, elles le sont rarement.
Contrôler le fichier avant de l’importer
En-tête annonçant utf8mb4, aucune séquence double dans le corps : voilà le dump sain, et cette vérification je l’expédie en dix secondes (de quoi vous épargner la reconstruction d’une base entière pour rien). Méfiance quand même, retoucher l’en-tête d’un dump déjà pourri ne donne rien, puisque les octets fautifs se trouvent dans le corps du fichier.
# Export en forçant le jeu de caractères de la connexion.
# Sans cette option, le client utilise le réglage du serveur, qui peut
# transcoder les données au passage sans aucun avertissement.
mysqldump --default-character-set=utf8mb4 \
--single-transaction --quick --routines \
-u UTILISATEUR -p NOM_BASE > boutique.sql
# En-tête écrit par mysqldump : la ligne SET NAMES doit annoncer utf8mb4
head -n 25 boutique.sql | grep -i -e "SET NAMES" -e "character_set"
# Le fichier lui-même doit être reconnu comme UTF-8
file -bi boutique.sql
# Repérer des séquences déjà doublement encodées dans le dump
grep -c 'é' boutique.sql || true
# Import sur la base cible, avec la même option
mysql --default-character-set=utf8mb4 -u UTILISATEUR -p NOUVELLE_BASE < boutique.sql
La collation réelle des tables et des colonnes
Entre une base restée franchement en latin1 d’un bout à l’autre et une base convertie à moitié, je préfère nettement la première : dans le second cas, les symptômes vont et viennent selon la page affichée.
Trois niveaux à inspecter
Trois endroits déclarent l’interclassement : la base elle-même, puis chaque table, puis chaque colonne de texte. En général, après une conversion menée table par table, je récupère des colonnes restées en arrière, celles qu’ont ajoutées vos modules en particulier (on les oublie presque toujours). Et ce sont elles qui déclencheront plus tard des erreurs sur les jointures mélangeant deux interclassements.
Cartographier avant de convertir
Faites l’inventaire des trois niveaux avant la moindre écriture, histoire de savoir combien d’objets vont bouger et lesquels portent du contenu visible en front-office, puis conservez ce relevé près de vous (il servira de comparaison une fois la conversion passée).
/* Remplacez ma_base par le nom de votre base et ps_ par votre préfixe. */ /* Interclassement de la base */ SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = 'ma_base'; /* Tables qui ne sont pas en utf8mb4 */ SELECT TABLE_NAME, TABLE_COLLATION, ENGINE, ROW_FORMAT FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'ma_base' AND TABLE_TYPE = 'BASE TABLE' AND TABLE_COLLATION NOT LIKE 'utf8mb4%' ORDER BY TABLE_NAME; /* Colonnes restées en retrait alors que leur table a été convertie */ SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, CHARACTER_SET_NAME, COLLATION_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'ma_base' AND COLLATION_NAME IS NOT NULL AND COLLATION_NAME NOT LIKE 'utf8mb4%' ORDER BY TABLE_NAME, ORDINAL_POSITION;
Convertir sans aggraver la corruption
Tout dépend de ce que contiennent réellement vos données : du vrai latin1, ou de l’UTF-8 caché derrière une étiquette latin1, et la commande de conversion change selon le cas. Confondez-les, la corruption double.
Deux situations, deux commandes
Vrais caractères latin1 dans une table latin1, et je laisse CONVERT TO CHARACTER SET utf8mb4 remettre les octets d’équerre. Table en latin1 qui héberge déjà de l’UTF-8, et la commande se mettrait à recoder ce qui ne doit surtout pas l’être : faites alors passer vos colonnes une à une par un type binaire, lequel se contente de changer l’étiquette (pas un seul octet réécrit).
Rester sur les interclassements acceptés
Du côté de l’installeur PrestaShop, deux interclassements seulement passent, utf8mb4_general_ci et utf8mb4_unicode_ci : prenez l’un des deux et laissez tomber celui que MySQL 8 propose par défaut, sans quoi vos jointures à cheval sur les deux familles vous renverront des erreurs tôt ou tard.
/* Sauvegarde obligatoire avant toute écriture. */
/* Cas 1 : la table est en latin1 et contient de vrais caractères latin1.
CONVERT TO recode proprement les octets, c'est la bonne commande.
L'installeur PrestaShop n'accepte que utf8mb4_general_ci et
utf8mb4_unicode_ci comme interclassement de base : tenez-vous-y. */
ALTER DATABASE `ma_base` CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
ALTER TABLE `ps_product_lang` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
/* Génération des commandes pour les tables restantes */
SELECT CONCAT('ALTER TABLE `', TABLE_NAME,
'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;') AS requete
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'ma_base'
AND TABLE_TYPE = 'BASE TABLE'
AND TABLE_COLLATION <> 'utf8mb4_unicode_ci';
/* Cas 2 : la table est déclarée latin1 mais contient déjà des octets UTF-8.
CONVERT TO recoderait ces octets et aggraverait la corruption. Le passage
par un type binaire ne touche pas aux octets, il ne change que l'étiquette.
Reprenez le type exact de la colonne tel qu'il figure dans le schéma. */
ALTER TABLE `ps_product_lang` MODIFY `name` VARBINARY(128);
ALTER TABLE `ps_product_lang` MODIFY `name`
VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL;
/* Erreur 1071, « Specified key was too long » : la table est en ROW_FORMAT
COMPACT ou REDUNDANT, plafonnée à 767 octets de préfixe d'index.
DYNAMIC autorise 3072 octets et c'est le format par défaut de MySQL 8. */
-- ALTER TABLE `ps_exemple` ROW_FORMAT=DYNAMIC;
Repérer le double encodage stocké en dur
Connexion propre, interclassements propres, et vos accents toujours en vrac ? Les octets portent la corruption, c’est là que je creuse. Elle y laisse une marque repérable.
La signature C383
Prenez du texte UTF-8, lisez-le comme du latin1, réencodez en UTF-8, et un à se colle devant chaque accent d’origine, or ce à occupe, pour information, les deux octets C3 et 83, un enchaînement que le français correct ne fabrique à peu près jamais. Votre filtre est là, et il ne rate pas les lignes à traiter.
/* Le double encodage laisse une signature stable. Le caractère Ã, soit U+00C3, s'écrit sur deux octets C3 83 en UTF-8. Un texte français sain ne produit pratiquement jamais cette séquence. */ SELECT id_product, id_lang, id_shop, LEFT(name, 80) AS extrait FROM ps_product_lang WHERE HEX(name) LIKE '%C383%' OR HEX(description_short) LIKE '%C383%' LIMIT 100; /* Volumétrie avant de décider d'une réparation en masse */ SELECT 'product_lang' AS source, COUNT(*) AS lignes FROM ps_product_lang WHERE HEX(name) LIKE '%C383%' UNION ALL SELECT 'category_lang', COUNT(*) FROM ps_category_lang WHERE HEX(name) LIKE '%C383%' UNION ALL SELECT 'cms_lang', COUNT(*) FROM ps_cms_lang WHERE HEX(content) LIKE '%C383%';
Mesurer l’étendue des dégâts
Faites le compte des lignes atteintes, une table après l’autre. Rien à décider avant ça. Lâchez une réparation sans filtre sur une base à moitié touchée et vous abîmerez les textes encore propres, raison pour laquelle on vise la signature et non la table complète. D’ailleurs, si le dégât tient à quelques fiches et que la source d’origine existe toujours, recréer les fiches produits en PHP revient souvent moins cher qu’un chantier de réparation généralisé.
Réparer sans perte de données
Pour réparer, on reprend le trajet dans l’autre sens : descendre la chaîne jusqu’à l’octet, la relire comme du latin1, la réinterpréter ensuite en utf8mb4. Trois conversions emboîtées. Testez d’abord.
Un contrôle de sûreté indispensable
Tout caractère qu’on ne sait pas représenter en latin1 (un emoji, un caractère cyrillique) devient un point d’interrogation au moment de la conversion intermédiaire : sortez ces lignes-là en premier et occupez-vous-en à part. Sans ça, votre réparation effacera du contenu en silence.
Une opération non répétable
En pratique, la signature sert de garde-fou et l’écriture devient idempotente : réparée une fois, la ligne perd la séquence et quitte le périmètre toute seule. Moi, j’y vais quand même table après table, colonne après colonne, avec une sauvegarde toute fraîche à chaque palier. Toucher directement à votre catalogue mérite cette prudence, qu’il s’agisse de remettre un accent d’aplomb ou d’aller changer la catégorie d’un produit via phpMyAdmin.
/* Réparation du double encodage : on redescend la chaîne au niveau octet,
on la relit en latin1, puis on la réinterprète en utf8mb4. */
/* Étape 1, contrôle de sûreté. La conversion vers latin1 remplace par un
point d'interrogation tout caractère qui n'y est pas représentable,
un emoji par exemple. Ces lignes doivent être traitées à part. */
SELECT id_product, id_lang, name
FROM ps_product_lang
WHERE HEX(name) LIKE '%C383%'
AND CONVERT(name USING latin1) LIKE '%?%'
AND name NOT LIKE '%?%';
/* Étape 2, aperçu avant écriture */
SELECT id_product,
name AS avant,
CONVERT(BINARY(CONVERT(name USING latin1)) USING utf8mb4) AS apres
FROM ps_product_lang
WHERE HEX(name) LIKE '%C383%'
LIMIT 20;
/* Étape 3, écriture, uniquement sur les lignes portant la signature.
Relancer cette requête une seconde fois casserait les textes déjà réparés,
d'où le filtre sur HEX, qui rend l'opération non répétable. */
UPDATE ps_product_lang
SET name = CONVERT(BINARY(CONVERT(name USING latin1)) USING utf8mb4)
WHERE HEX(name) LIKE '%C383%';
Aligner le serveur MySQL ou MariaDB
La machine est à vous ? Coupez la cause à la racine, plutôt que de recoller les symptômes transfert après transfert. Moi, je fais comme ça.
Trois sections à renseigner
Trois entrées à remplir dans my.cnf : le client, l’outil mysql, le serveur. En mutualisé, ce fichier vous restera inaccessible (voilà où la méthode bute), et je retombe alors sur l’option en ligne de commande, au coup par coup.
# Fichier /etc/mysql/my.cnf ou /etc/my.cnf selon la distribution. # Sur un hébergement mutualisé ce fichier n'est pas accessible : dans ce cas, # l'option --default-character-set des outils en ligne de commande suffit. # Bloc à ajouter : # # [client] # default-character-set = utf8mb4 # # [mysql] # default-character-set = utf8mb4 # # [mysqld] # character-set-server = utf8mb4 # collation-server = utf8mb4_unicode_ci # Redémarrage du service sudo systemctl restart mysql # Contrôle des valeurs effectivement actives mysql -u UTILISATEUR -p -e "SHOW VARIABLES LIKE 'character\_set\_%';" mysql -u UTILISATEUR -p -e "SHOW VARIABLES LIKE 'collation\_%';"
Puis valider en conditions réelles
Une fois le service relancé, je repasse voir les variables de session telles que PrestaShop les lit. À vous ensuite : enregistrez un produit accentué, sortez une facture PDF, déclenchez un courriel transactionnel, faites tourner la recherche interne, et finissez par reconstruire votre index de recherche depuis le back-office. Du nettoyage de ce genre, mes notes de terrain en gardent beaucoup, et sur une base vraiment amochée, un travail sur mesure passe avant le script tout fait.
Questions fréquentes
Pourquoi des caractères étranges apparaissent-ils après une migration ?
Parce que quelque part dans la chaîne, du latin1 a été lu là où se trouvaient des octets UTF-8, avant un nouvel encodage en UTF-8. Deux moments classiques : l’export passé sans option de jeu de caractères, et l’import vers une base dont l’interclassement n’est pas celui du départ. Du coup vos tables portent la corruption en dur, et aucun réglage d’affichage n’y changera rien, ce sont les données qu’il faut reprendre.
Où se règle le jeu de caractères de la connexion dans PrestaShop ?
Nulle part. Aucune clé database_charset ne traîne dans app/config/parameters.php. La classe de connexion pose charset=utf8mb4 dans le DSN et lance SET NAMES utf8mb4, cela à partir de la version 1.7.8. En 1.7.6 et au-dessous, c’était utf8, donc utf8mb3. Autrement dit tout se décide sur le serveur MySQL et dans l’interclassement de vos tables (les fichiers de la boutique sont hors de cause).
Comment détecter un double encodage sans tout parcourir ?
Traquez C383, la séquence hexadécimale, dans vos colonnes texte : ce à planté devant chaque accent d’origine d’un texte doublement encodé s’écrit justement sur ces deux octets-là en UTF-8. Un filtre du genre HEX(name) LIKE '%C383%' sort les lignes concernées, et vous voyez l’ampleur du dégât avant la première écriture. Attaquez par ps_product_lang, ps_category_lang et ps_cms_lang.
La réparation binaire peut-elle perdre des données ?
Oui, dans une situation bien précise. Au passage intermédiaire par latin1, chaque caractère que ce jeu ne sait pas représenter (un emoji, un caractère cyrillique) se change en point d’interrogation. Commencez par lister les lignes où CONVERT(colonne USING latin1) fait surgir un point d’interrogation qui ne figurait pas dans le texte d’origine, puis traitez-les à l’écart. Pour tout le reste, accrochez votre écriture à la signature C383, les textes déjà sains resteront intacts.
Quelle collation choisir pour une boutique PrestaShop ?
Deux interclassements de base passent la rampe à l’installation, utf8mb4_general_ci et utf8mb4_unicode_ci. Choisissez-en un, puis déroulez-le partout, base, tables, colonnes. Celui que MySQL 8 pose par défaut, utf8mb4_0900_ai_ci, sort de cette liste et vous vaudra des écarts sur les jointures avec les tables restées en utf8mb4_unicode_ci.
Que faire face à l’erreur « Specified key was too long » ?
Traduction : votre table tourne sous ROW_FORMAT COMPACT ou REDUNDANT, deux formats qui butent à 767 octets de préfixe d’index, soit 191 caractères une fois en utf8mb4. Basculez-la vers ROW_FORMAT=DYNAMIC et vous montez à 3072 octets (format retenu par défaut depuis MySQL 5.7). Les vieux tutoriels parlent encore de la variable innodb_large_prefix : MySQL 8 l’a retirée, le comportement étendu y allant de soi.