Mise à niveau Pimcore 10 vers 12 : Le vrai parcours de migration
Le guide définitif pour migrer Pimcore 10 vers 12. PHP 8.3, Symfony 7, DBAL 4, migration de collation, système d'événements, ExtJS 7, Redis, OpenSearch, Kubernetes, Flysystem et chaque bug non documenté qu'on a trouvé.
Pourquoi tu ne peux pas sauter Pimcore 11
La première question que chaque équipe pose : est-ce qu'on peut passer directement de la 10 à la 12 ? Non. Pimcore 11 est le passage obligatoire. Les changements de framework sont trop importants pour être absorbés en une seule étape, et l'outillage de migration de Pimcore lui-même suppose que la version intermédiaire est en place.
Voici la portée complète de ce qui change entre les deux versions majeures :
| Composant | Pimcore 10.6 | Pimcore 11 | Pimcore 12 |
|---|---|---|---|
| PHP | 8.0+ | 8.1+ | 8.3+ |
| Symfony | 5.4 | 6.2+ | 6.4 / 7.x |
| Doctrine DBAL | 2.x / 3.x | 3.x | 4.x |
| Admin UI | Intégré (ExtJS 6) | Bundle séparé (ExtJS 6/7) | Bundle séparé (ExtJS 7) + Studio UI (React) |
| Licence | GPLv3 | GPLv3 | POCL (Pimcore Open Core License) |
| Bundles core | Monolithique | Extraits | Extraits |
| Backend de recherche | Intégré | Simple Backend Search Bundle | Generic Data Index + OpenSearch |
| WYSIWYG | TinyMCE intégré | Bundle TinyMCE | Bundle TinyMCE ou Quill |
| Enregistrement produit | Aucun | Aucun | Requis (validation hors ligne) |
| Système d'événements (JS) | Plugin Broker | Event Listeners | Event Listeners + événements Studio UI |
| Collation | utf8mb4_general_ci | utf8mb4_general_ci | utf8mb4_unicode_520_ci (requis) |
| Cache | Adaptateur Pimcore | Adaptateur Symfony | Adaptateur Symfony (changement de config) |
| Full page cache | Intégré | Intégré | Option bundle séparé |
| Stockage des assets | Local / Flysystem | Local / Flysystem | Local / Flysystem (avec bugs sur remote) |
On a réalisé cette mise à niveau sur plusieurs installations Pimcore entreprise, allant de PIM B2B à des plateformes CMS multi-sites. Les patterns décrits ici sont génériques. Pour comprendre comment on aborde les implémentations PIM et l'architecture système, ces guides couvrent notre méthodologie plus large. Notre page méthodologie explique comment on planifie ce genre de migrations à haut risque.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Pimcore 10.6 │ ──▶ │ Pimcore 11.x │ ──▶ │ Pimcore 12.x │
│ Symfony 5.4 │ │ Symfony 6.2+ │ │ Symfony 6.4/7 │
│ PHP 8.0+ │ │ PHP 8.1+ │ │ PHP 8.3+ │
│ DBAL 2.x/3.x │ │ DBAL 3.x │ │ DBAL 4.x │
│ ExtJS 6 │ │ ExtJS 6/7 │ │ ExtJS 7 + │
│ │ │ │ │ Studio UI │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Phase 1 : Préparation (avant de toucher à Composer)
C'est la phase qui prend le plus de temps et qui en fait gagner le plus. Chaque heure passée ici épargne des jours de débogage plus tard.
1.1 Checklist pré-mise à niveau
# Vérifier la version actuelle
bin/console --version
# S'assurer que toutes les migrations sont à jour
bin/console doctrine:migrations:up-to-date
bin/console doctrine:migrations:migrate
# Vider toutes les files de messages avant la mise à niveau
# Les workers doivent finir leur traitement avant de modifier le codebase
bin/console messenger:consume --limit=0
Sauvegarde tout : base de données (mysqldump complet), fichiers (répertoire assets, répertoire var, répertoire config), et crée une branche de mise à niveau dédiée dans le contrôle de version. Teste ta procédure de restauration avant de commencer.
1.2 Méthodes Doctrine dépréciées (DBAL 2/3 vers 4)
C'est l'étape de préparation la plus chronophage. Chaque classe repository, chaque requête SQL brute, chaque service custom qui touche à la base de données doit être mis à jour. Dans une installation entreprise typique, attends-toi à 50-150 endroits à modifier.
| Déprécié (DBAL 2/3) | Remplacement (DBAL 4) | Notes |
|---|---|---|
$db->query($sql) | $db->executeQuery($sql) | Le type de retour devient Result |
$db->executeUpdate($sql) | $db->executeStatement($sql) | Retourne le nombre de lignes affectées en int |
$db->fetchRow() | $db->fetchAssociative() | Retourne un tableau associatif ou false |
$db->fetchAll() | $db->fetchAllAssociative() | Retourne un tableau de tableaux associatifs |
$db->fetchCol() | $db->fetchFirstColumn() | Retourne un tableau indexé |
$db->fetchColumn() | $db->fetchOne() | Récupération d'une valeur unique |
$db->fetchPairs() | Pimcore\Db\Helper::fetchPairs() | Helper Pimcore |
$db->insertOrUpdate() | Pimcore\Db\Helper::upsert() | Helper Pimcore |
$db->quoteInto() | Pimcore\Db\Helper::quoteInto() | Helper Pimcore |
$db->deleteWhere() | $db->executeStatement() avec DELETE | SQL manuel |
$db->updateWhere() | $db->executeStatement() avec UPDATE | SQL manuel |
$db->quote($val, $type) | $db->quote($val) | Chaînes uniquement dans DBAL 4, pas de paramètre type |
Pimcore\Db\ConnectionInterface | Doctrine\DBAL\Connection | Changement de type hint en DI |
Pimcore\Db\Connection | Doctrine\DBAL\Connection | Remplacement de classe |
# Trouver tous les appels de méthodes dépréciées dans ton codebase
grep -rn "->query(" src/ --include="*.php"
grep -rn "->fetchRow(" src/ --include="*.php"
grep -rn "->fetchAll(" src/ --include="*.php" | grep -v "fetchAllAssociative"
grep -rn "->fetchCol(" src/ --include="*.php"
grep -rn "->insertOrUpdate(" src/ --include="*.php"
grep -rn "->quoteInto(" src/ --include="*.php"
grep -rn "->deleteWhere(" src/ --include="*.php"
grep -rn "->executeUpdate(" src/ --include="*.php"
grep -rn "Pimcore\\Db\\Connection" src/ --include="*.php"
Exemple complet de migration pour une classe repository :
// AVANT (Pimcore 10 / DBAL 2-3)
use Pimcore\Db\ConnectionInterface;
class PriceExporter
{
public function __construct(
private readonly ConnectionInterface $connection
) {}
public function export(int $limit, int $offset): array
{
$sql = "SELECT p.o_id, p.price FROM object_store_1 p WHERE p.o_published = 1 LIMIT ? OFFSET ?";
return $this->connection->query($sql)->fetchAll();
}
}
// APRÈS (Pimcore 12 / DBAL 4)
use Doctrine\DBAL\Connection;
class PriceExporter
{
public function __construct(
private readonly Connection $connection
) {}
public function export(int $limit, int $offset): array
{
$sql = "SELECT p.id, p.price FROM object_store_1 p WHERE p.published = 1 LIMIT ? OFFSET ?";
return $this->connection->executeQuery($sql, [$limit, $offset])->fetchAllAssociative();
}
}
Mets à jour ton services.yaml si tu utilises l'injection de dépendances explicite :
services:
App\Export\PriceExporter:
arguments:
$connection: '@doctrine.dbal.default_connection'
1.3 Déclarations de types de retour
Pimcore 11+ impose des types de retour stricts sur les classes modèles. Chaque classe qui étend les classes de base Pimcore doit être mise à jour :
// Avant (Pimcore 10)
public function save()
{
// logique custom
parent::save();
}
// Après (Pimcore 11+)
public function save(array $parameters = []): static
{
// logique custom
return parent::save($parameters);
}
Ça affecte chaque classe modèle DataObject, chaque listing custom et chaque service qui surcharge les méthodes Pimcore. Vérifie toutes les classes dans ton répertoire src/Model/.
1.4 Suppression du préfixe o_
Pimcore 11 supprime le préfixe o_ des noms de colonnes de la table objects. Chaque requête SQL brute et chaque condition de listing qui référence ces colonnes doit être mise à jour :
| Ancien (Pimcore 10) | Nouveau (Pimcore 11+) | Contexte |
|---|---|---|
o_id | id (ou oo_id dans les tables object store) | Clé primaire |
o_creationDate | creationDate | Horodatage |
o_modificationDate | modificationDate | Horodatage |
o_path | path | Chemin dans l'arborescence |
o_key | key | Clé/slug de l'objet |
o_published | published | Indicateur de publication |
o_parentId | parentId | Référence parent |
o_type | type | Type d'objet |
o_className | className | Nom de classe |
o_classId | classId | ID de classe |
o_userOwner | userOwner | ID utilisateur créateur |
o_userModification | userModification | ID du dernier modificateur |
o_versionCount | versionCount | Compteur de versions |
# Trouver toutes les utilisations du préfixe o_ dans PHP et JavaScript
grep -rn "o_id\|o_path\|o_key\|o_published\|o_parentId\|o_type\|o_className\|o_classId" src/ --include="*.php"
grep -rn "o_className\|o_id" public/ --include="*.js"
Exemple de migration d'une condition de listing :
// Avant
$listing->setCondition('o_path LIKE ? AND o_published = 1', ['/products/%']);
// Après
$listing->setCondition('path LIKE ? AND published = 1', ['/products/%']);
1.5 Changements cassants PHP 8.3 / 8.4
Au-delà de ce que Pimcore exige, PHP 8.3+ introduit ses propres changements cassants :
Paramètres implicitement nullables (déprécié en 8.4) :
// DÉPRÉCIÉ : nullable implicite
public function findByLocale(string $locale = null): array
// CORRIGÉ : nullable explicite
public function findByLocale(?string $locale = null): array
Ça affecte chaque signature de méthode avec = null comme valeur par défaut. Dans un codebase entreprise typique, attends-toi à 50+ occurrences. Utilise Rector pour automatiser :
// rector.php
use Rector\Config\RectorConfig;
use Rector\Php84\Rector\Param\ExplicitNullableParamTypeRector;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([__DIR__ . '/src']);
$rectorConfig->rule(ExplicitNullableParamTypeRector::class);
};
# Prévisualiser les changements
vendor/bin/rector process --dry-run
# Appliquer les changements
vendor/bin/rector process
Autres fonctionnalités PHP 8.3+ que tu peux adopter :
// Constantes de classe typées (PHP 8.3)
public const string DEFAULT_LOCALE = 'en';
public const array SUPPORTED_TYPES = ['product', 'category'];
// Attribut #[\Override] (PHP 8.3)
#[\Override]
public function normalize(mixed $object, ?string $format = null, array $context = []): array
// json_validate() (PHP 8.3)
if (json_validate($input)) { /* ... */ }
1.6 Migration du système d'événements JavaScript
L'ancien système Plugin Broker est supprimé dans Pimcore 11. Tout le JavaScript de l'admin UI doit être migré :
// AVANT (Pimcore 10 - Plugin Broker)
pimcore.plugin.datasheet = Class.create(pimcore.plugin.admin, {
getClassName: function () {
return "pimcore.plugin.datasheet";
},
initialize: function () {
pimcore.plugin.broker.registerPlugin(this);
},
postOpenObject: function (object, type) {
if (object.data.general.o_className === 'Product') {
// Ajouter un bouton à la barre d'outils
}
}
});
// APRÈS (Pimcore 11+ - Event Listeners)
(function() {
'use strict';
document.addEventListener(pimcore.events.postOpenObject, (e) => {
let object = e.detail.object;
if (object.data.general.className === 'Product') { // o_className -> className
object.toolbar.add({
text: t('Generate PDF'),
iconCls: 'pimcore_icon_pdf',
handler: function () {
Ext.Ajax.request({
url: '/api/generate-pdf/' + object.id,
success: function (response) {
pimcore.helpers.showNotification(
t("success"), t("PDF generated"), "success"
);
}
});
}
});
pimcore.layout.refresh();
}
});
})();
Autres changements JavaScript :
| Ancien (Pimcore 10) | Nouveau (Pimcore 11+) | Notes |
|---|---|---|
ts() | t() | Fonction de traduction |
pimcore.helpers.addCsrfTokenToUrl() | Supprimer entièrement | CSRF géré différemment |
o_className en JS | className | Correspond au changement PHP |
Class.create(...) | Toujours supporté, mais préférer les classes ES6 | Pérennisation |
/admin/tags/* | Peut changer en /pimcore-admin/tags/* | Vérifie tes routes admin |
1.7 Changements ExtJS 7 (Pimcore 12)
Pimcore 12 utilise ExtJS 7.x. Changements cassants clés pour le code admin UI custom :
// ExtJS 6 (Pimcore 10/11)
var store = new Ext.data.Store({
proxy: {
type: 'ajax',
reader: {
type: 'json',
root: 'data' // DÉPRÉCIÉ dans ExtJS 7
}
}
});
// ExtJS 7 (Pimcore 12)
var store = new Ext.data.Store({
proxy: {
type: 'ajax',
reader: {
type: 'json',
rootProperty: 'data' // Utiliser rootProperty
}
}
});
Vérifie tous les opérateurs de grille custom, les plugins admin et les extensions d'importation de données pour la compatibilité ExtJS 7. Problèmes courants :
- La propriété
rootrenommée enrootPropertydans les readers de store - Certaines configurations de colonnes de grille ont changé
- Le patching de prototype (
Ext.override) peut casser si la structure interne des classes a changé - Vérifie que toutes les références
iconClsexistent toujours dans le jeu d'icônes de Pimcore 12
1.8 Pimcore Studio UI (React)
Pimcore 12 introduit la nouvelle Studio UI construite avec React, fonctionnant en parallèle de l'admin classique ExtJS. Ce n'est pas encore un remplacement mais un ajout :
- Studio UI gère certaines nouvelles fonctionnalités (UI du Generic Data Index, une partie de la gestion des assets)
- L'Admin UI classique (ExtJS) reste l'interface principale pour la plupart des opérations
- Les plugins ExtJS custom continuent de fonctionner dans l'admin classique
- Les nouveaux points d'extension pour Studio UI utilisent des composants React
- La direction à long terme est vers React, mais une migration complète n'est pas requise pour P12
Si tu as des extensions admin UI custom, elles continueront de fonctionner dans l'admin classique. Aucune migration immédiate vers React n'est nécessaire. Mais si tu construis de nouvelles fonctionnalités admin, envisage de les construire pour Studio UI à la place. On a écrit un walkthrough détaillé sur la construction d'un bundle Pimcore 12 production-ready avec intégration Studio UI qui couvre le processus complet, du squelette de bundle à l'enregistrement de composants React.
1.9 Supprimer SensioFrameworkExtraBundle
Ce bundle est déprécié et supprimé dans Symfony 6. Remplace toutes les annotations :
// Avant (Sensio)
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
/** @Template("default/default.html.twig") */
public function defaultAction()
{
return [];
}
// Après (Symfony 6+)
public function defaultAction(): Response
{
return $this->render('default/default.html.twig', []);
}
1.10 Annotations de route Symfony vers attributs
Les 15+ fichiers contrôleur ont tous besoin de ce changement :
// Avant (annotations Symfony 5.4)
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/{_locale}/{slug}_p{id}",
* name="product_detail",
* methods={"GET"},
* requirements={"slug"="[\w\s\-\.]+", "id"="[\w\. ]+"}
* )
*/
public function __invoke(Request $request, string $id): Response
// Après (attributs Symfony 6.4+)
use Symfony\Component\Routing\Attribute\Route;
#[Route(
path: '/{_locale}/{slug}_p{id}',
name: 'product_detail',
methods: ['GET'],
requirements: ['slug' => '[\w\s\-\.]+', 'id' => '[\w\. ]+']
)]
public function __invoke(Request $request, string $id): Response
Mets à jour config/packages/routing.yaml :
# Changer le type de 'annotation' à 'attribute'
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute
1.11 Changements du Serializer Symfony (Symfony 7.x)
Si tu as des Normalizers custom (courant pour l'indexation de recherche, la sérialisation API), Symfony 7.x exige une nouvelle méthode :
// Avant (Symfony 5.4/6.x)
class ProductNormalizer implements NormalizerInterface
{
public function normalize(mixed $object, string $format = null, array $context = []): array
{
// ...
}
public function supportsNormalization(mixed $data, string $format = null): bool
{
return $data instanceof Product;
}
}
// Après (Symfony 7.x)
class ProductNormalizer implements NormalizerInterface
{
public function normalize(mixed $object, ?string $format = null, array $context = []): array
{
// ...
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof Product;
}
// NOUVEAU : Requis dans Symfony 7.x
public function getSupportedTypes(?string $format): array
{
return [
Product::class => true,
];
}
}
Si tu as 10+ normalizers (typique pour l'indexation MeiliSearch ou OpenSearch), c'est un changement répétitif mais critique. L'absence de getSupportedTypes() cause une erreur fatale sur Symfony 7.x.
1.12 Dépréciation de Request::get()
Request::get() est déprécié dans Symfony 6.x. Utilise les méthodes d'accès spécifiques :
// Déprécié
$value = $request->get('param');
// Utilise les méthodes spécifiques
$value = $request->query->get('param'); // Paramètres GET
$value = $request->request->get('param'); // Paramètres POST
$value = $request->attributes->get('param'); // Paramètres de route
$value = $request->headers->get('param'); // En-têtes
$value = $request->query->getBoolean('flag'); // Paramètre GET booléen
1.13 Installer le pont de compatibilité
composer require --no-update pimcore/compatibility-bridge-v10
composer require --no-update symfony/dotenv
Phase 2 : Mise à niveau vers Pimcore 11
2.1 Mise à jour Composer
{
"require": {
"pimcore/pimcore": "^11.0",
"pimcore/admin-ui-classic-bundle": "^1.0"
}
}
composer remove --no-update sensio/framework-extra-bundle
COMPOSER_MEMORY_LIMIT=-1 composer update -W
Attends-toi à ce que la résolution des dépendances lutte avec les bundles tiers. Contacte les éditeurs de bundles pour les versions compatibles P11 ou trouve des alternatives.
2.2 Enregistrer le bundle Admin
// src/Kernel.php
public function registerBundlesToCollection(BundleCollection $collection): void
{
$collection->addBundle(new \Pimcore\Bundle\AdminBundle\PimcoreAdminBundle(), 60);
}
2.3 Installer les bundles extraits
Beaucoup de fonctionnalités core sont maintenant des bundles séparés. Installe uniquement ce que tu utilises réellement :
composer require \
pimcore/static-routes-bundle \
pimcore/custom-reports-bundle \
pimcore/application-logger-bundle \
pimcore/seo-bundle \
pimcore/simple-backend-search-bundle \
pimcore/glossary-bundle \
pimcore/tinymce-bundle \
pimcore/xliff-bundle \
pimcore/word-export-bundle
Enregistre chacun dans config/bundles.php.
2.4 Mettre à jour la configuration des routes
# config/routes.yaml
_pimcore:
resource: "@PimcoreCoreBundle/config/routing.yaml" # était : Resources/config/routing.yml
# config/routes/dev/routes.yaml
_pimcore:
resource: "@PimcoreCoreBundle/config/routing_dev.yaml" # était : Resources/config/routing_dev.yml
2.5 Configuration du transport Messenger
Pimcore 11 introduit de nouveaux transports de messages asynchrones :
framework:
messenger:
transports:
# Tes transports existants...
# NOUVEAU pour Pimcore 11+
pimcore_scheduled_tasks:
dsn: "%messenger.dsn%/pimcore_scheduled_tasks"
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 2
pimcore_image_optimize:
dsn: "%messenger.dsn%/pimcore_image_optimize"
pimcore_asset_update:
dsn: "%messenger.dsn%/pimcore_asset_update"
pimcore_search_backend_message:
dsn: "%messenger.dsn%/pimcore_search_backend_message"
Si tu utilises RabbitMQ (AMQP), chaque transport crée une file séparée. Si tu utilises le transport Doctrine, chacun crée une partition de table séparée. Planifie le déploiement des workers en conséquence.
2.6 Exécuter les migrations et reconstruire
bin/console doctrine:migrations:migrate
bin/console pimcore:cache:clear
bin/console cache:clear
rm -rf var/cache/*
Teste tout à ce point de contrôle avant de poursuivre.
Si tu es aussi en train de repenser les workflows pendant cette mise à niveau, lis notre guide sur la conception de workflows Pimcore entreprise pour les patterns architecturaux.
Phase 3 : Mise à niveau vers Pimcore 12
3.1 Licence et enregistrement du produit
Pimcore 12 introduit deux changements obligatoires :
Licence : POCL (Pimcore Open Core License) remplace GPLv3. L'édition Community avec le bundle Admin UI Classic nécessite une licence ExtJS (environ 1 480 EUR).
| Édition | Prix |
|---|---|
| Community (POCL) | Gratuit (chiffre d'affaires sous 5 M EUR) |
| Professional | 8 400 EUR/an |
| Enterprise Auto-hébergé | 25 200 EUR/an |
| Enterprise PaaS | À partir de 39 900 USD/an |
Enregistrement produit : Obligatoire. Trois variables d'environnement requises :
| Variable | Objectif |
|---|---|
PIMCORE_ENCRYPTION_SECRET | Clé de chiffrement Defuse, générée une fois par installation |
PIMCORE_PRODUCT_KEY | Clé d'enregistrement depuis license.pimcore.com |
PIMCORE_ENTERPRISE_TOKEN | Token d'authentification Composer pour les paquets privés |
Étapes d'enregistrement :
# 1. Générer le secret de chiffrement (une fois par installation, NE JAMAIS changer après)
vendor/bin/generate-defuse-key
# 2. Calculer le hash d'enregistrement
php -r "echo hash_hmac('sha256', 'your-instance-id', 'your-encryption-secret');"
# 3. S'enregistrer sur license.pimcore.com avec le hash
# URL : https://license.pimcore.com/register?instance_identifier=ID&instance_hash=HASH
# 4. Recevoir la clé produit, la définir comme variable d'environnement PIMCORE_PRODUCT_KEY
# config/config.yaml
pimcore:
encryption:
secret: '%env(PIMCORE_ENCRYPTION_SECRET)%'
product_registration:
product_key: '%env(PIMCORE_PRODUCT_KEY)%'
instance_identifier: 'your-instance-id'
La validation est entièrement hors ligne. Elle s'exécute à chaque build du container Symfony (cache clear, démarrage de l'application). Elle vérifie la signature ECDSA de la clé produit contre une clé publique livrée avec Pimcore. Aucun accès internet nécessaire. Si la validation échoue, une InvalidConfigurationException est levée et l'application refuse de démarrer.
Critique : ne change jamais le secret de chiffrement après la configuration initiale. Toutes les données chiffrées en base de données en dépendent. Traite-le comme un mot de passe de base de données. Chaque installation séparée (staging, production) peut partager la même clé si elles partagent le même identifiant d'instance.
3.2 Mise à jour Composer
composer require -W pimcore/pimcore:^12.0
3.3 Configuration de la sécurité
security:
# SUPPRIMER (par défaut dans Symfony 6.4+) :
# enable_authenticator_manager: true
firewalls:
pimcore_admin_webdav:
pattern: ^/asset/webdav # était : ^/admin/asset/webdav
access_control:
- { path: ^/asset/webdav, roles: ROLE_PIMCORE_USER }
3.4 Éditeur WYSIWYG
# Option A : TinyMCE (Professional/Enterprise)
composer require pimcore/tinymce-bundle
# Option B : Quill (gratuit, édition Community)
composer require pimcore/quill-bundle
3.5 Changements finaux Doctrine DBAL 4
Au-delà des remplacements de méthodes de la Phase 1, mets à jour la configuration Doctrine :
# config/packages/doctrine.yaml
doctrine:
dbal:
connections:
default:
driver: "pdo_mysql"
server_version: "mariadb-10.11" # Sois précis
charset: utf8mb4
default_table_options:
charset: utf8mb4
collate: utf8mb4_unicode_520_ci # Défaut P12
Configuration Redis en profondeur
Redis remplit quatre fonctions critiques dans un déploiement Pimcore 12 en production. Se tromper dans la configuration cause des échecs silencieux difficiles à diagnostiquer.
1. Cache applicatif (Tag-Aware)
# config/packages/cache.yaml
framework:
cache:
# Pimcore 10 (ANCIEN)
# pools:
# pimcore.cache.pool:
# tags: true # SUPPRIMER
# adapter: pimcore.cache.adapter.redis_tag_aware # ANCIEN nom d'adaptateur
# Pimcore 11+/12 (NOUVEAU)
pools:
pimcore.cache.pool:
public: true
default_lifetime: 31536000 # 1 an
adapter: cache.adapter.redis_tag_aware # Nom d'adaptateur changé
provider: 'redis://%env(REDIS_HOST)%:%env(REDIS_PORT)%'
Le changement de nom de l'adaptateur de pimcore.cache.adapter.redis_tag_aware à cache.adapter.redis_tag_aware est silencieux. L'ancien nom ne lève pas d'erreur. Le cache retombe simplement sur le système de fichiers, et tu te demandes pourquoi ton déploiement multi-pods a des données périmées partout.
Sans cache Redis, chaque rendu de page interroge la base de données pour les arbres de navigation, les paramètres du site, les définitions de classes, les traductions, et plus encore. Sur un déploiement Kubernetes multi-pods, chaque pod maintient son propre cache filesystem sans coordination d'invalidation. Redis rend l'invalidation du cache cohérente entre tous les pods.
2. Stockage des sessions
# config/packages/framework.yaml
framework:
session:
handler_id: '%env(REDIS_SESSION_DSN)%'
# Exemple : redis://redis:6379/1
Pour les déploiements multi-pods, les sessions doivent être partagées. Sans sessions Redis, les utilisateurs sont déconnectés quand leur requête arrive sur un pod différent (load balancing round-robin). C'est le symptôme le plus visible d'une configuration Redis manquante.
3. Full Page Cache
Le full page cache de Pimcore stocke le HTML rendu pour les utilisateurs anonymes. Dans Pimcore 12, il peut utiliser Redis comme backend :
# config/packages/pimcore.yaml
pimcore:
full_page_cache:
enabled: true
lifetime: 7200 # 2 heures
exclude_patterns:
- '/admin'
- '/api'
exclude_cookie: 'pimcore_admin_sid'
Le full page cache réduit considérablement la charge de la base de données pour les pages publiques. Une page qui prend 200 ms à rendre depuis la base de données se sert en moins de 5 ms depuis le cache. Sur les sites à fort trafic, c'est la différence entre avoir besoin de 2 pods web et d'en avoir besoin de 10.
L'invalidation du cache se fait automatiquement quand les éditeurs publient du contenu. Mais attention au contenu personnalisé : toute page qui varie selon le contexte utilisateur (état de connexion, locale, variante de test A/B) doit être exclue du full page cache ou utiliser des fragments ESI.
4. Transport Messenger (optionnel)
framework:
messenger:
transports:
pimcore_core:
dsn: 'redis://%env(REDIS_HOST)%:%env(REDIS_PORT)%/messages'
Redis Streams peut remplacer RabbitMQ comme transport de messages. Plus simple à opérer (un service en moins), mais pas de dead letter queues, de priorités de messages ni de routage d'exchange. Pour les installations avec des topologies de workers complexes, RabbitMQ reste préférable. Pour les configurations plus simples, Redis Streams fonctionne bien.
Dimensionnement Redis
| Échelle Pimcore | Mémoire Redis | Notes |
|---|---|---|
| Petit (< 10K objets) | 256 Mo | Instance unique |
| Moyen (10K-100K objets) | 1-2 Go | Instance unique, persistance activée |
| Grand (100K+ objets) | 4-8 Go | Envisager Redis Sentinel ou Cluster |
Surveille used_memory et evicted_keys. Si des clés sont évincées, ton cache est trop petit et tu perds en performance.
La migration de collation (la partie la plus difficile)
C'est la section qui fait gagner le plus de temps. La migration de collation est la partie la plus dangereuse et la moins documentée de toute la mise à niveau.
Pourquoi ça casse
Les migrations Pimcore 12 créent de nouvelles tables et modifient les colonnes existantes avec la collation utf8mb4_unicode_520_ci. Ta base de données existante de Pimcore 10 utilise la collation par défaut du serveur MySQL. Sur Azure MySQL 8.0, c'est utf8mb4_0900_ai_ci. Sur les configurations plus anciennes, c'est peut-être utf8mb4_general_ci.
Le résultat : des collations mélangées au sein d'une même table et entre les tables. Quand Pimcore exécute des requêtes UNION entre les tables objects, assets et documents :
-- C'est ce que le getRelationData() de Pimcore exécute
SELECT r.dest_id, r.type, o.className as subtype, concat(o.path, o.`key`) as `path`
FROM objects o, object_relations_X r WHERE ...
UNION
SELECT r.dest_id, r.type, a.type as subtype, concat(a.path, a.filename) as `path`
FROM assets a, object_relations_X r WHERE ...
UNION
SELECT r.dest_id, r.type, d.type as subtype, concat(d.path, d.`key`) as `path`
FROM documents d, object_relations_X r WHERE ...
Si objects.className est en utf8mb4_unicode_520_ci (venant de la migration P12) mais assets.type est en utf8mb4_0900_ai_ci (venant du défaut Azure), MySQL lève :
SQLSTATE[HY000]: General error: 1267 Illegal mix of collations for operation 'UNION'
Cette erreur apparaît quand tu ouvres n'importe quel DataObject dans l'admin UI. Le système est de fait inutilisable.
Distribution réelle que tu trouveras
Sur une base de données migrée de P10 via P11 vers P12 sur Azure MySQL 8.0 :
| Collation | Tables | Colonnes | Source |
|---|---|---|---|
utf8mb4_0900_ai_ci | ~580 | ~5 700 | Défaut Azure MySQL 8.0 |
utf8mb4_unicode_ci | ~87 | ~155 | Anciennes tables de bundles |
utf8mb4_unicode_520_ci | ~6 | ~994 | Créées par la migration P12 |
utf8mb3_general_ci | ~4 | ~31 | Très anciennes tables legacy |
utf8mb4_general_ci | ~1 | ~3 | Tables de bundles spécifiques |
# Vérifier ta distribution actuelle
bin/console doctrine:query:sql "SELECT TABLE_COLLATION, COUNT(*) AS cnt FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_COLLATION IS NOT NULL GROUP BY TABLE_COLLATION ORDER BY cnt DESC"
# Vérification au niveau des colonnes (c'est là que le vrai problème se cache)
bin/console doctrine:query:sql "SELECT COLLATION_NAME, COUNT(*) AS cnt FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND COLLATION_NAME IS NOT NULL GROUP BY COLLATION_NAME ORDER BY cnt DESC"
Cinq cas spéciaux qui cassent un simple ALTER TABLE
1. Tables avec index sur path (assets, documents, objects, http_error_log) :
Les index composites sur path + key/filename dépassent la limite d'index InnoDB de 3072 octets après la conversion de collation (utf8mb4 = 4 octets/caractère, 255 caractères x 4 = 1020 octets par colonne, composite = au-dessus de la limite).
-- Supprimer les index, convertir, recréer avec des longueurs de préfixe
DROP INDEX fullpath ON objects;
DROP INDEX type_path_classId ON objects;
ALTER TABLE objects CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;
CREATE INDEX fullpath ON objects (path(255), `key`(255));
CREATE INDEX type_path_classId ON objects (type, path(255), classId);
2. Tables classification store : Contrainte FK entre object_classificationstore_data_* et object_classificationstore_groups_*. La FK doit être supprimée avant la conversion et recréée après. Le nom de colonne peut être id (P12) ou o_id (P10/11) selon l'état de migration.
3. Tables de traduction (translations_admin, translations_messages) : Le changement de collation peut exposer des doublons insensibles à la casse. Si tu as à la fois "MyKey" et "mykey" comme clés de traduction séparées (valide sous une collation sensible à la casse), la conversion vers une collation insensible à la casse cause une violation de contrainte unique. On a trouvé environ 1 600 doublons dans une seule base de données de staging. Les doublons doivent être supprimés avant la conversion.
4. Table assets : Il faut recréer l'index fullpath comme non unique. Des noms de fichiers qui diffèrent par la casse peuvent exister (par exemple, EEVA.tif et Eeva.tif sont des fichiers distincts légitimes). Un index unique rejetterait le second après le changement de collation.
5. Tables volumineuses : Les tables avec des millions de lignes (les tables de requêtes localisées, par exemple, peuvent avoir 30+ tables par classe avec une par locale) prennent un temps significatif pour ALTER. Sur une base de données avec 680 tables, la conversion complète prend environ 18 minutes.
Exécuter la migration
Exécute pendant une fenêtre de maintenance avec les pods web mis à l'arrêt :
# Réduire à zéro pour éviter les contentions de verrous sur les tables de traduction
kubectl scale deployment pimcore pimcore-frontend -n <namespace> --replicas=0
kubectl get pods -n <namespace> -l app=pimcore -w # Attendre la terminaison
Après avoir exécuté le script de migration automatisé (qui gère les cinq cas spéciaux) :
# Reconstruire les définitions de classes (restaure les contraintes FK)
PIMCORE_CLASS_DEFINITION_WRITABLE=1 bin/console pimcore:deployment:classes-rebuild -c
# Vérifier qu'il ne reste qu'une seule collation
bin/console doctrine:query:sql "SELECT TABLE_COLLATION, COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_COLLATION IS NOT NULL GROUP BY TABLE_COLLATION"
# Remonter les réplicas
kubectl scale deployment pimcore -n <namespace> --replicas=1
kubectl scale deployment pimcore-frontend -n <namespace> --replicas=3
Référence rapide : erreurs de collation
| Erreur | Cause | Correction |
|---|---|---|
Illegal mix of collations for operation 'UNION' | Collations mélangées entre tables dans les requêtes de relation | Convertir toutes les tables en utf8mb4_unicode_520_ci |
Specified key was too long; max key length is 3072 bytes | L'index composite path+key dépasse la limite | Supprimer l'index, convertir, recréer avec des longueurs de préfixe |
Referencing column and referenced column are incompatible | FK entre tables avec des collations différentes | Supprimer la FK, convertir les deux tables, reconstruire les classes |
Duplicate entry for key 'PRIMARY' | Le changement de collation rend des clés de casse différente identiques | Supprimer les doublons avant la conversion |
Lock wait timeout exceeded | L'application maintient des verrous sur les tables de traduction | Arrêter les pods avant ALTER |
Optimisation de la base de données après migration
Après la migration de collation, exécute OPTIMIZE sur toutes les tables pour défragmenter le stockage et reconstruire les index :
# Tables core
bin/console doctrine:query:sql "OPTIMIZE TABLE assets, documents, objects, versions, translations_admin, translations_messages, search_backend_data, properties, dependencies"
# Vérifier la fragmentation élevée
bin/console doctrine:query:sql "SELECT TABLE_NAME, TABLE_ROWS, ROUND(DATA_LENGTH/1024/1024,2) AS data_mb, ROUND(DATA_FREE/1024/1024,2) AS free_mb, ROUND(DATA_FREE/(DATA_LENGTH+1)*100,1) AS frag_pct FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND DATA_FREE > DATA_LENGTH * 0.1 AND DATA_LENGTH > 1048576 ORDER BY DATA_FREE DESC"
Configuration SSL MySQL
Pour les services MySQL managés (Azure, AWS RDS) avec require_secure_transport=ON :
# config/packages/prod/doctrine.yaml
doctrine:
dbal:
connections:
default:
options:
!php/const PDO::MYSQL_ATTR_SSL_CA: true
!php/const PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT: false
MYSQL_ATTR_SSL_CA: true active SSL sans spécifier de chemin de certificat. Le serveur fournit son certificat et le client lui fait confiance. MYSQL_ATTR_SSL_VERIFY_SERVER_CERT: false saute la vérification du hostname (sûr pour le réseau interne).
Pour savoir comment on gère les migrations de base de données dans notre pratique plus large d'ingénierie de données, cette page couvre notre méthodologie.
Intégration OpenSearch
Pimcore 12 introduit le bundle Generic Data Index, qui utilise OpenSearch (ou Elasticsearch) pour la recherche backend et l'indexation des données.
Configuration des index (doit se faire avant les migrations)
# Créer les index vides manuellement (requis avant l'exécution des migrations)
curl -X PUT "http://opensearch-cluster:9200/pimcore_document" -H 'Content-Type: application/json' -d '{}'
curl -X PUT "http://opensearch-cluster:9200/pimcore_asset" -H 'Content-Type: application/json' -d '{}'
# NE PAS créer manuellement les index de statistiques du Portal Engine
# Ils se créent automatiquement avec des noms basés sur le temps : {name}__{year}_{month}
# La création manuelle entre en conflit avec le pattern d'alias
C'est critique parce que certaines migrations P12 déclenchent Document->save(), qui fire l'événement DocumentIndexUpdateSubscriber. Si l'index n'existe pas : index_not_found_exception suivi d'erreurs SAVEPOINT en cascade qui corrompent l'état de migration.
Après que les migrations sont terminées :
# Créer les index pour toutes les classes DataObject
php -d memory_limit=2G bin/console generic-data-index:deployment:reindex --no-interaction
# Commandes disponibles
bin/console list generic-data-index
# generic-data-index:deployment:reindex Crée/met à jour les index pour les classDefinitions
# generic-data-index:reindex Déclenche la réindexation native
# generic-data-index:update:index Met à jour l'index/mapping
Dimensionnement du cluster OpenSearch
| Échelle de l'installation | Nœuds | Mémoire/Nœud | Stockage/Nœud | Java Heap |
|---|---|---|---|---|
| Petit (< 10K objets) | 1 | 4 Go | 50 Go SSD | 2 Go |
| Moyen (10K-100K) | 2 | 8 Go | 100 Go SSD | 4 Go |
| Grand (100K+) | 3 (master-eligible) | 16 Go | 200 Go SSD | 8 Go |
Si tu remplaces Elasticsearch 7.x, OpenSearch est un remplacement direct. L'API est compatible. Seule la bibliothèque cliente et les chemins de configuration changent. Pour savoir comment on conçoit l'architecture de recherche plus largement, consulte notre guide des plateformes e-commerce.
Sécurité OpenSearch
Pour la communication interne au cluster (pods dans le même namespace Kubernetes), tu peux désactiver le plugin de sécurité :
# opensearch-cluster.yaml
additionalConfig:
plugins.security.disabled: "true"
Pour l'accès externe ou les clusters multi-tenants, configure une authentification appropriée.
Bugs Flysystem et stockage distant
Si tu utilises du stockage d'assets distant (Azure Blob, AWS S3, Google Cloud Storage), Pimcore 12 a deux bugs que tu dois connaître.
Bug 1 : getDimensions() télécharge chaque image à chaque rendu de page
Affecte : Pimcore 10.6+, 11.x, 12.x avec stockage Flysystem distant
Chaque appel à thumbnail()->html() déclenche getDimensions(), qui appelle readDimensionsFromFile() avant d'essayer getEstimatedDimensions(). Sur du stockage distant, chaque lecture de fichier coûte 50-100 ms d'I/O réseau.
Impact mesuré : Une page avec environ 80 références d'images prenait 5 735 ms (79 appels au stockage distant à environ 65 ms chacun). Après avoir corrigé l'ordre des méthodes, la même page se rendait en 170 ms avec zéro appel distant.
Cause racine dans ImageThumbnailTrait::getDimensions() :
// Ordre actuel (INCORRECT pour le stockage distant)
// Étape 1 : Vérifier le cache thumbnail en BDD (rapide, mais souvent un miss)
// Étape 2 : readDimensionsFromFile() → TÉLÉCHARGE LE FICHIER DEPUIS LE CLOUD (~65ms)
// Étape 3 : getEstimatedDimensions() → JAMAIS ATTEINT (l'étape 2 réussit toujours)
// Ordre corrigé
if (empty($dimensions) && $config && $asset instanceof Image) {
$dimensions = $config->getEstimatedDimensions($asset); // ~0ms, calcul pur
}
if (empty($dimensions) && $this->exists()) {
$dimensions = $this->readDimensionsFromFile(); // 50-100ms, dernier recours uniquement
}
getEstimatedDimensions() calcule les dimensions à partir des dimensions de l'image originale (stockées en base de données) et de la configuration de transformation du thumbnail. Du calcul pur, zéro I/O. En production, 95%+ des images ont des dimensions stockées. Ce correctif élimine pratiquement tous les I/O de stockage distant pendant le rendu des pages.
Bug 2 : Les thumbnails de prévisualisation de dossier bloqués sur le spinner de chargement
Affecte : admin-ui-classic-bundle 2.2.x, 2.3.x
Après avoir uploadé des images, l'onglet Prévisualisation affiche un spinner de chargement qui ne se résout jamais. Le navigateur met en cache le GIF placeholder de façon permanente parce que l'URL utilise modificationDate comme cache buster (ne change jamais), et aucune génération asynchrone de thumbnail n'est dispatchée pour l'origine folderPreview (contrairement à treeNode qui le dispatche).
Correction : Ajouter Cache-Control: no-store à la réponse placeholder et dispatcher AssetPreviewImageMessage.
Déploiement Kubernetes
Architecture des pods
| Pod | Objectif | Réplicas |
|---|---|---|
pimcore-web | Nginx + PHP-FPM, admin + frontend | 2-4 |
pimcore-worker | Consumers Symfony Messenger | 1-3 |
pimcore-ops | Pod CLI de maintenance (migrations, rebuilds) | 1 |
redis | Cache, sessions, transport optionnel | 1+ |
mysql | Base de données (ou service managé) | 1+ |
opensearch | Index de recherche | 2-3 |
rabbitmq | Broker de messages (si pas Redis) | 1-3 |
Gestion des secrets
apiVersion: v1
kind: Secret
metadata:
name: pimcore-secrets
type: Opaque
data:
PIMCORE_ENCRYPTION_SECRET: <base64>
PIMCORE_PRODUCT_KEY: <base64>
PIMCORE_ENTERPRISE_TOKEN: <base64>
DATABASE_HOST: <base64>
DATABASE_NAME: <base64>
DATABASE_USER: <base64>
DATABASE_PASSWORD: <base64>
REDIS_HOST: <base64>
REDIS_PORT: <base64>
APP_SECRET: <base64>
Utilise Sealed Secrets (Bitnami) ou des opérateurs de secrets externes pour la production. Ne commite jamais des secrets en clair dans Git.
Chaque init container (create-assets, create-classes, db-migrations, clear-cache) doit avoir TOUS les secretRefs dans sa liste envFrom. Les secrets manquants causent des erreurs Environment variable not found silencieuses.
Déploiement des workers
spec:
replicas: 2
template:
spec:
containers:
- name: worker
command: ["php", "bin/console", "messenger:consume",
"pimcore_core", "pimcore_maintenance", "pimcore_scheduled_tasks",
"pimcore_image_optimize", "pimcore_asset_update",
"pimcore_search_backend_message",
"--time-limit=3600", "--memory-limit=512M"]
--time-limit et --memory-limit garantissent que les workers redémarrent périodiquement, évitant les fuites de mémoire PHP. Kubernetes redémarre le pod quand le processus se termine proprement.
Ordre de déploiement
1. Déployer les ressources Kubernetes
2. Shell dans le pod ops
3. Installer les bundles manquants (PimcoreAdminBundle, GenericExecutionEngine, etc.)
4. Créer les index OpenSearch manuellement (pimcore_document, pimcore_asset)
5. Exécuter la réindexation DataObject OpenSearch
6. Exécuter les migrations de base de données (avec PIMCORE_CLASS_DEFINITION_WRITABLE=1)
7. Exécuter la migration de collation (fenêtre de maintenance, pods web arrêtés)
8. Exécuter l'optimisation de la base de données (OPTIMIZE TABLE)
9. Commandes post-déploiement (classes-rebuild, reindex, assets:install, cache:clear)
10. Vérifier : ouvrir un DataObject dans l'admin, vérifier les assets, vérifier le frontend
Nos services cloud couvrent le déploiement et les opérations Kubernetes pour Pimcore et d'autres plateformes.
Architecture du système d'événements dans Pimcore 12
Comprendre le système d'événements de Pimcore est essentiel à la fois pour la mise à niveau et pour la construction d'extensions.
Événements PHP (backend)
Pimcore utilise l'EventDispatcher de Symfony. Événements clés pour les DataObjects :
use Pimcore\Event\DataObjectEvents;
// Événements disponibles
DataObjectEvents::PRE_ADD // Avant la première sauvegarde
DataObjectEvents::POST_ADD // Après la première sauvegarde
DataObjectEvents::PRE_UPDATE // Avant chaque sauvegarde
DataObjectEvents::POST_UPDATE // Après chaque sauvegarde
DataObjectEvents::PRE_DELETE // Avant la suppression
DataObjectEvents::POST_DELETE // Après la suppression
Créer un event subscriber dans Pimcore 12 :
use Pimcore\Event\Model\DataObjectEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ProductEventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
DataObjectEvents::POST_UPDATE => 'onPostUpdate',
];
}
public function onPostUpdate(DataObjectEvent $event): void
{
$object = $event->getObject();
if (!$object instanceof Product) {
return;
}
// Indexer pour la recherche, générer des assets, etc.
}
}
Événements JavaScript (Admin UI)
Pimcore 12 utilise un pattern de souscription aux événements différent de P10 :
// Événements admin Pimcore 12
document.addEventListener(pimcore.events.preOpenObject, (e) => {
// Avant l'ouverture de l'éditeur d'objet
});
document.addEventListener(pimcore.events.postOpenObject, (e) => {
// Après l'ouverture de l'éditeur d'objet, e.detail.object disponible
});
document.addEventListener(pimcore.events.preSaveObject, (e) => {
// Avant la sauvegarde, peut annuler avec e.preventDefault()
});
document.addEventListener(pimcore.events.postSaveObject, (e) => {
// Après la fin de la sauvegarde
});
Étendre Pimcore 12
Les bundles custom dans Pimcore 12 suivent les conventions de bundle Symfony :
// src/CustomBundle/CustomBundle.php
namespace App\CustomBundle;
use Pimcore\Extension\Bundle\AbstractPimcoreBundle;
class CustomBundle extends AbstractPimcoreBundle
{
public function getJsPaths(): array
{
return [
'/bundles/custom/js/startup.js',
];
}
public function getCssPaths(): array
{
return [
'/bundles/custom/css/admin.css',
];
}
}
Enregistre le JavaScript et le CSS admin custom dans la configuration du bundle :
# config/packages/pimcore_admin.yaml
pimcore_admin:
assets:
js:
- '/bundles/custom/js/startup.js'
- '/bundles/custom/js/grid-operators.js'
css:
- '/bundles/custom/css/admin.css'
Vérification post-mise à niveau
Séquence de commandes
PIMCORE_CLASS_DEFINITION_WRITABLE=1 php -d memory_limit=2G bin/console pimcore:deployment:classes-rebuild -c
php -d memory_limit=2G bin/console generic-data-index:deployment:reindex --no-interaction
bin/console assets:install public
php -d memory_limit=2G bin/console pimcore:search-backend-reindex
bin/console messenger:setup-transports
bin/console cache:clear
bin/console cache:warmup
Matrice de tests fonctionnels
| Domaine | Quoi tester | Comment vérifier |
|---|---|---|
| Connexion admin | Login, 2FA, déconnexion | Manuellement dans le navigateur |
| DataObjects | Créer, modifier, sauvegarder, publier, supprimer, versions | Ouvrir un objet, modifier un champ, sauvegarder |
| Assets | Upload, thumbnails, métadonnées, téléchargement | Uploader une image, vérifier la génération du thumbnail |
| Documents | Éditer les pages, areabricks, sauvegarder, publier | Éditer une page avec des areabricks |
| Relations | Relations d'objets, relations d'assets | Ouvrir un objet avec des relations, vérifier que la requête UNION fonctionne |
| Recherche | Recherche backend, indexation OpenSearch | Rechercher un objet dans la barre d'outils admin |
| Messenger | Traitement des files, tous les transports | Vérifier bin/console messenger:stats |
| Cache | Connectivité Redis, invalidation | Publier du contenu, vérifier que le changement est visible sur l'autre pod |
| Full page cache | Cache de pages anonymes, invalidation | Charger la page, vérifier les en-têtes de réponse pour un cache hit |
| Intégration ERP | Traitement des imports, mapping de champs | Exécuter un import de test |
| Permissions | RBAC, permissions de workflow | Se connecter en tant qu'utilisateur restreint, vérifier l'accès |
| JS custom | Opérateurs de grille, plugins admin | Ouvrir la vue grille, vérifier que les colonnes custom fonctionnent |
Benchmarks de performance
| Métrique | Acceptable | Avertissement | Critique |
|---|---|---|---|
| Chargement page admin | < 2 s | 2-5 s | > 5 s |
| Ouverture DataObject | < 3 s | 3-8 s | > 8 s |
| Upload asset (10 Mo) | < 5 s | 5-15 s | > 15 s |
| Réponse recherche | < 500 ms | 500 ms-2 s | > 2 s |
| Page frontend (cache) | < 50 ms | 50-200 ms | > 200 ms |
| Page frontend (sans cache) | < 500 ms | 500 ms-2 s | > 2 s |
| Rendu thumbnail (stockage distant) | < 200 ms | 200 ms-1 s | > 1 s (vérifier le bug getDimensions) |
Pièges courants
-
Sauter Pimcore 11. Tu ne peux pas passer de la 10 à la 12 directement. La version intermédiaire gère les changements critiques de schéma, l'extraction des bundles et la suppression des préfixes.
-
Exécuter les migrations sans les index OpenSearch. Certaines migrations déclenchent des event subscribers qui interrogent OpenSearch.
index_not_found_exceptioncascade en un état SAVEPOINT corrompu. Crée les index d'abord. -
Ne pas corriger les collations. Les collations mélangées causent des échecs de requêtes UNION quand tu ouvres n'importe quel DataObject. Ça casse l'admin UI silencieusement jusqu'à ce qu'un éditeur ouvre un produit.
-
Perdre le secret de chiffrement. S'il est perdu après le déploiement, toutes les données chiffrées en base de données deviennent irrécupérables. Ce n'est pas récupérable. Traite-le comme le mot de passe maître de la base de données.
-
Fallback silencieux du cache Redis. Le changement de nom de l'adaptateur de
pimcore.cache.adapter.redis_tag_awareàcache.adapter.redis_tag_awarene lève pas d'erreur. Le cache retombe silencieusement sur le filesystem. Les déploiements multi-pods cassent. -
Ne pas mettre à jour les transports Messenger. Les transports P11 manquants font que les tâches planifiées, le traitement des assets et l'indexation de recherche cessent silencieusement de fonctionner.
-
Exécuter la migration de collation avec les pods web actifs. La conversion des tables de traduction crée des deadlocks en cas d'accès concurrent. Arrête les pods d'abord.
-
Créer manuellement les index de statistiques du Portal Engine. La création automatique utilise des noms d'index basés sur le temps avec des alias. La création manuelle entre en conflit avec le pattern d'alias.
-
Tester sur une base de données vide. La migration de schéma sur une base de données vide cache 90% des problèmes. Utilise un clone complet des données de production.
-
Ignorer le bug getDimensions() sur le stockage cloud. Chaque rendu de page non mis en cache est 3 à 7 secondes plus lent que nécessaire tant que ce n'est pas patché. Le correctif tient en deux lignes à réordonner.
-
Méthode
getSupportedTypes()manquante sur les normalizers. Symfony 7.x exige cette méthode. Son absence cause une erreur fatale, pas un avertissement de dépréciation. -
Ne pas mettre à jour les appels
Request::get(). Déprécié mais fonctionne encore dans Symfony 6.x. Cassera dans 7.x. -
Oublier
PIMCORE_CLASS_DEFINITION_WRITABLE=1. Les migrations et reconstructions de classes échouent silencieusement sans cette variable d'environnement. Le message d'erreur est clair, mais c'est facile à oublier. -
ExtJS 7
rootvsrootProperty. Les readers de store custom utilisantroot: 'data'échouent silencieusement à parser les réponses dans ExtJS 7.
Points clés à retenir
-
Trois phases, pas de raccourcis. Préparation (corriger tout le code déprécié), Pimcore 11 (mise à niveau framework + extraction des bundles), Pimcore 12 (enregistrement, DBAL 4, collation, système d'événements). Chaque phase est un point de contrôle.
-
La migration de collation est la partie la plus difficile. Cinq types de tables spéciales nécessitent un traitement custom. Prévois une fenêtre de maintenance. Utilise un script automatisé. Vérifie au niveau des tables et des colonnes.
-
L'enregistrement produit est obligatoire et hors ligne. Pas d'internet nécessaire pour la validation. Mais le secret de chiffrement est irrécupérable s'il est perdu.
-
Redis n'est pas optionnel dans les déploiements multi-pods. La cohérence du cache, le partage des sessions, le full page cache et optionnellement le transport de messages en dépendent tous. Le changement de nom de l'adaptateur est un changement cassant silencieux.
-
Les index OpenSearch doivent exister avant les migrations. Crée les index document et asset manuellement, puis exécute la réindexation des data objects. L'ordre compte.
-
La migration du système d'événements est mécanique mais critique. Plugin Broker vers Event Listeners,
o_classNameversclassName,@Routevers#[Route],Request::get()vers les accesseurs spécifiques. Rector peut automatiser 80% du travail. -
Le stockage distant expose des bugs Pimcore. Le bug d'ordonnancement de getDimensions() ajoute des secondes à chaque rendu de page. Le bug de cache de prévisualisation de dossier bloque les thumbnails de façon permanente. Les deux ont des correctifs simples.
-
Teste sur un clone de données de production. À chaque fois. Une mise à niveau sur base de données vide prend 20 minutes et réussit trivialement. Les vrais problèmes apparaissent avec 70K assets, 680 tables avec des collations mélangées et 50K+ data objects.
C'est exactement le genre de travail d'ingénierie logicielle que notre équipe gère régulièrement. Si tu planifies une mise à niveau Pimcore, notre équipe de consulting a réalisé cette opération pour plusieurs installations entreprise dans la région DACH. On le propose aussi dans le cadre de nos services plus larges.
Prêt à démarrer ta mise à niveau Pimcore ? Parle à notre équipe ou demande un devis.
Sujets couverts
Guides connexes
Guide Entreprise des Systèmes d'IA Agentiques
Guide technique des systemes d'IA agentiques en entreprise. Decouvre l'architecture, les capacites et les applications des agents IA autonomes.
Lire le guideCommerce Agentique : Comment laisser les agents IA acheter en toute securite
Comment concevoir un commerce agentique gouverne. Moteurs de politiques, portes d'approbation HITL, reçus HMAC, idempotence, isolation multi-tenant et le protocole Agentic Checkout complet.
Lire le guideLes 9 endroits où ton système IA laisse fuir des données (et comment colmater chacun)
Cartographie systématique de chaque point de fuite de données dans les systèmes IA. Prompts, embeddings, logs, appels d'outils, mémoire d'agent, messages d'erreur, cache, données de fine-tuning et transferts entre agents.
Lire le guidePrêt à construire des systèmes IA prêts pour la production ?
Notre équipe est spécialisée dans les systèmes IA prêts pour la production. Discutons de comment nous pouvons aider.
Démarrer une conversation