Pimcore Upgrade 10 auf 12: Der echte Migrationspfad
Der definitive Leitfaden zum Upgrade von Pimcore 10 auf 12. PHP 8.3, Symfony 7, DBAL 4, Collation-Migration, Event-System, ExtJS 7, Redis, OpenSearch, Kubernetes, Flysystem und jeder undokumentierte Bug.
Warum du Pimcore 11 nicht überspringen kannst
Die erste Frage, die jedes Team stellt: Können wir direkt von 10 auf 12 springen? Nein. Pimcore 11 ist der Pflichtzwischenschritt. Die Framework-Änderungen sind zu umfangreich, um sie in einem Schritt zu absorbieren, und Pimcores eigene Migrations-Tooling setzt die Zwischenversion voraus.
Hier ist der volle Umfang der Änderungen über die beiden Major-Versionen:
| Komponente | 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 | Built-in (ExtJS 6) | Separates Bundle (ExtJS 6/7) | Separates Bundle (ExtJS 7) + Studio UI (React) |
| Lizenz | GPLv3 | GPLv3 | POCL (Pimcore Open Core License) |
| Core Bundles | Monolithisch | Extrahiert | Extrahiert |
| Such-Backend | Built-in | Simple Backend Search Bundle | Generic Data Index + OpenSearch |
| WYSIWYG | TinyMCE built-in | TinyMCE Bundle | TinyMCE oder Quill Bundle |
| Produktregistrierung | Keine | Keine | Erforderlich (Offline-Validierung) |
| Event-System (JS) | Plugin Broker | Event Listeners | Event Listeners + Studio UI Events |
| Collation | utf8mb4_general_ci | utf8mb4_general_ci | utf8mb4_unicode_520_ci (erforderlich) |
| Cache | Pimcore Adapter | Symfony Adapter | Symfony Adapter (Konfigurationsänderung) |
| Full Page Cache | Built-in | Built-in | Separates Bundle (optional) |
| Asset-Speicher | Lokal / Flysystem | Lokal / Flysystem | Lokal / Flysystem (mit Bugs bei Remote) |
Wir haben dieses Upgrade an mehreren Enterprise-Pimcore-Installationen durchgeführt, von B2B-PIMs bis hin zu Multi-Site-CMS-Plattformen. Die hier beschriebenen Muster sind produktunabhängig. Für Kontext dazu, wie wir PIM-Implementierungen und Systemarchitektur angehen, decken diese Guides unsere breitere Methodik ab. Unsere Methodikseite erklärt, wie wir solche hochriskanten Migrationen planen.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 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: Vorbereitung (Bevor du Composer anfasst)
Diese Phase dauert am längsten und spart die meiste Zeit. Jede Stunde hier investiert spart Tage an Debugging später.
1.1 Pre-Upgrade-Checkliste
# Aktuelle Version prüfen
bin/console --version
# Sicherstellen, dass alle Migrations aktuell sind
bin/console doctrine:migrations:up-to-date
bin/console doctrine:migrations:migrate
# Alle Message-Queues leeren vor dem Upgrade
# Worker müssen die Verarbeitung abschliessen, bevor du die Codebase änderst
bin/console messenger:consume --limit=0
Sichere alles: Datenbank (kompletter mysqldump), Dateien (Assets-Verzeichnis, var-Verzeichnis, config-Verzeichnis), und erstelle einen dedizierten Upgrade-Branch in der Versionskontrolle. Teste deine Wiederherstellungsprozedur bevor du anfängst.
1.2 Veraltete Doctrine-Methoden (DBAL 2/3 auf 4)
Das ist der arbeitsintensivste Vorbereitungsschritt. Jede Repository-Klasse, jede Raw-SQL-Query, jeder Custom-Service der die Datenbank berührt, muss aktualisiert werden. Bei einer typischen Enterprise-Installation rechne mit 50-150 Stellen.
| Veraltet (DBAL 2/3) | Ersatz (DBAL 4) | Hinweise |
|---|---|---|
$db->query($sql) | $db->executeQuery($sql) | Rückgabetyp ändert sich zu Result |
$db->executeUpdate($sql) | $db->executeStatement($sql) | Gibt betroffene Zeilenanzahl als int zurück |
$db->fetchRow() | $db->fetchAssociative() | Gibt assoziatives Array oder false zurück |
$db->fetchAll() | $db->fetchAllAssociative() | Gibt Array von assoziativen Arrays zurück |
$db->fetchCol() | $db->fetchFirstColumn() | Gibt indiziertes Array zurück |
$db->fetchColumn() | $db->fetchOne() | Einzelwert-Abfrage |
$db->fetchPairs() | Pimcore\Db\Helper::fetchPairs() | Pimcore Helper |
$db->insertOrUpdate() | Pimcore\Db\Helper::upsert() | Pimcore Helper |
$db->quoteInto() | Pimcore\Db\Helper::quoteInto() | Pimcore Helper |
$db->deleteWhere() | $db->executeStatement() mit DELETE | Manuelles SQL |
$db->updateWhere() | $db->executeStatement() mit UPDATE | Manuelles SQL |
$db->quote($val, $type) | $db->quote($val) | Nur Strings in DBAL 4, kein Type-Parameter |
Pimcore\Db\ConnectionInterface | Doctrine\DBAL\Connection | Type-Hint-Änderung in DI |
Pimcore\Db\Connection | Doctrine\DBAL\Connection | Klassen-Ersetzung |
# Alle veralteten Methodenaufrufe in deiner Codebase finden
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"
Vollständiges Migrationsbeispiel für eine Repository-Klasse:
// VORHER (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();
}
}
// NACHHER (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();
}
}
Aktualisiere deine services.yaml, falls du explizite Dependency Injection verwendest:
services:
App\Export\PriceExporter:
arguments:
$connection: '@doctrine.dbal.default_connection'
1.3 Return-Type-Deklarationen
Pimcore 11+ erzwingt strikte Return-Types bei Model-Klassen. Jede Klasse, die Pimcore-Basisklassen erweitert, muss aktualisiert werden:
// Vorher (Pimcore 10)
public function save()
{
// Custom-Logik
parent::save();
}
// Nachher (Pimcore 11+)
public function save(array $parameters = []): static
{
// Custom-Logik
return parent::save($parameters);
}
Das betrifft jede DataObject-Model-Klasse, jedes Custom Listing und jeden Service der Pimcore-Methoden überschreibt. Prüfe alle Klassen in deinem src/Model/-Verzeichnis.
1.4 Das o_-Prefix entfernen
Pimcore 11 entfernt das o_-Prefix von Datenbank-Spaltennamen in der objects-Tabelle. Jede Raw-SQL-Query und Listing-Condition, die diese Spalten referenziert, muss aktualisiert werden:
| Alt (Pimcore 10) | Neu (Pimcore 11+) | Kontext |
|---|---|---|
o_id | id (oder oo_id in Object-Store-Tabellen) | Primärschlüssel |
o_creationDate | creationDate | Zeitstempel |
o_modificationDate | modificationDate | Zeitstempel |
o_path | path | Baumpfad |
o_key | key | Object-Key/Slug |
o_published | published | Veröffentlichungs-Flag |
o_parentId | parentId | Parent-Referenz |
o_type | type | Objekttyp |
o_className | className | Klassenname |
o_classId | classId | Klassen-ID |
o_userOwner | userOwner | Ersteller-User-ID |
o_userModification | userModification | Letzte Bearbeiter-User-ID |
o_versionCount | versionCount | Versionszähler |
# Alle o_-Prefix-Verwendungen in PHP und JavaScript finden
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"
Beispiel für die Migration einer Listing-Condition:
// Vorher
$listing->setCondition('o_path LIKE ? AND o_published = 1', ['/products/%']);
// Nachher
$listing->setCondition('path LIKE ? AND published = 1', ['/products/%']);
1.5 PHP 8.3 / 8.4 Breaking Changes
Neben den Pimcore-Anforderungen bringt PHP 8.3+ eigene Breaking Changes mit:
Implizit nullable Parameter (deprecated in 8.4):
// DEPRECATED: implizit nullable
public function findByLocale(string $locale = null): array
// KORRIGIERT: explizit nullable
public function findByLocale(?string $locale = null): array
Das betrifft jede Methodensignatur mit = null als Default. In einer typischen Enterprise-Codebase rechne mit 50+ Vorkommen. Rector kann das automatisieren:
// 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);
};
# Vorschau der Änderungen
vendor/bin/rector process --dry-run
# Änderungen anwenden
vendor/bin/rector process
Weitere PHP 8.3+ Features, die du einsetzen kannst:
// Typisierte Klassenkonstanten (PHP 8.3)
public const string DEFAULT_LOCALE = 'en';
public const array SUPPORTED_TYPES = ['product', 'category'];
// #[\Override]-Attribut (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 JavaScript Event-System-Migration
Das alte Plugin-Broker-System wird in Pimcore 11 entfernt. Alle Admin-UI-JavaScript-Dateien müssen migriert werden:
// VORHER (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') {
// Toolbar-Button hinzufügen
}
}
});
// NACHHER (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();
}
});
})();
Weitere JavaScript-Änderungen:
| Alt (Pimcore 10) | Neu (Pimcore 11+) | Hinweise |
|---|---|---|
ts() | t() | Übersetzungsfunktion |
pimcore.helpers.addCsrfTokenToUrl() | Komplett entfernen | CSRF wird anders behandelt |
o_className in JS | className | Entspricht der PHP-Änderung |
Class.create(...) | Weiterhin unterstützt, aber ES6-Klassen bevorzugen | Zukunftssicher |
/admin/tags/* | Kann sich zu /pimcore-admin/tags/* ändern | Admin-Routen prüfen |
1.7 ExtJS 7 Änderungen (Pimcore 12)
Pimcore 12 nutzt ExtJS 7.x. Wichtige Breaking Changes für Custom-Admin-UI-Code:
// ExtJS 6 (Pimcore 10/11)
var store = new Ext.data.Store({
proxy: {
type: 'ajax',
reader: {
type: 'json',
root: 'data' // DEPRECATED in ExtJS 7
}
}
});
// ExtJS 7 (Pimcore 12)
var store = new Ext.data.Store({
proxy: {
type: 'ajax',
reader: {
type: 'json',
rootProperty: 'data' // rootProperty verwenden
}
}
});
Prüfe alle Custom Grid Operators, Admin-Plugins und Data-Importer-Extensions auf ExtJS-7-Kompatibilität. Häufige Probleme:
root-Property umbenannt zurootPropertyin Store Readers- Einige Grid-Column-Configs haben sich geändert
- Prototype-Patching (
Ext.override) kann brechen, falls sich die interne Klassenstruktur geändert hat - Prüfe, ob alle
iconCls-Referenzen noch im Icon-Set von Pimcore 12 existieren
1.8 Pimcore Studio UI (React)
Pimcore 12 führt die neue Studio UI ein, gebaut mit React, die parallel zum klassischen ExtJS-Admin läuft. Das ist noch kein Ersatz, sondern eine Ergänzung:
- Studio UI übernimmt einige neue Features (Generic Data Index UI, Teile des Asset-Managements)
- Classic Admin UI (ExtJS) bleibt die primäre Oberfläche für die meisten Operationen
- Custom ExtJS-Plugins funktionieren weiterhin im klassischen Admin
- Neue Extension Points für Studio UI nutzen React-Komponenten
- Langfristiger Kurs ist React, aber eine vollständige Migration ist für P12 nicht erforderlich
Wenn du Custom-Admin-UI-Extensions hast, funktionieren sie weiterhin im klassischen Admin. Eine sofortige Migration zu React ist nicht nötig. Aber wenn du neue Admin-Funktionalität baust, ziehe in Betracht, sie für die Studio UI zu entwickeln. Wir haben einen ausführlichen Walkthrough zum Erstellen eines produktionstauglichen Pimcore 12 Bundles mit Studio UI Integration geschrieben, der den gesamten Prozess vom Bundle-Skeleton bis zur React-Komponenten-Registrierung abdeckt.
1.9 SensioFrameworkExtraBundle entfernen
Dieses Bundle ist deprecated und in Symfony 6 entfernt. Ersetze alle Annotations:
// Vorher (Sensio)
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
/** @Template("default/default.html.twig") */
public function defaultAction()
{
return [];
}
// Nachher (Symfony 6+)
public function defaultAction(): Response
{
return $this->render('default/default.html.twig', []);
}
1.10 Symfony Route Annotations zu Attributes
Alle 15+ Controller-Dateien brauchen diese Änderung:
// Vorher (Symfony 5.4 Annotations)
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
// Nachher (Symfony 6.4+ Attributes)
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
Aktualisiere config/packages/routing.yaml:
# Typ von 'annotation' auf 'attribute' ändern
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute
1.11 Symfony Serializer Änderungen (Symfony 7.x)
Falls du Custom Normalizers hast (üblich für Suchindexierung, API-Serialisierung), erfordert Symfony 7.x eine neue Methode:
// Vorher (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;
}
}
// Nachher (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;
}
// NEU: Erforderlich in Symfony 7.x
public function getSupportedTypes(?string $format): array
{
return [
Product::class => true,
];
}
}
Falls du 10+ Normalizer hast (typisch für MeiliSearch- oder OpenSearch-Indexierung), ist das eine repetitive aber kritische Änderung. Fehlende getSupportedTypes() verursacht einen Fatal Error in Symfony 7.x.
1.12 Request::get() Deprecation
Request::get() ist deprecated in Symfony 6.x. Verwende spezifische Accessor-Methoden:
// Deprecated
$value = $request->get('param');
// Spezifische Methoden verwenden
$value = $request->query->get('param'); // GET-Parameter
$value = $request->request->get('param'); // POST-Parameter
$value = $request->attributes->get('param'); // Route-Parameter
$value = $request->headers->get('param'); // Headers
$value = $request->query->getBoolean('flag'); // Boolean GET-Parameter
1.13 Compatibility Bridge installieren
composer require --no-update pimcore/compatibility-bridge-v10
composer require --no-update symfony/dotenv
Phase 2: Upgrade auf Pimcore 11
2.1 Composer Update
{
"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
Rechne damit, dass die Dependency-Auflösung bei Third-Party-Bundles Probleme macht. Kontaktiere Bundle-Hersteller für P11-kompatible Versionen oder finde Alternativen.
2.2 Admin Bundle registrieren
// src/Kernel.php
public function registerBundlesToCollection(BundleCollection $collection): void
{
$collection->addBundle(new \Pimcore\Bundle\AdminBundle\PimcoreAdminBundle(), 60);
}
2.3 Extrahierte Bundles installieren
Viele Core-Features sind jetzt separate Bundles. Installiere nur was du tatsächlich nutzt:
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
Registriere jedes Bundle in config/bundles.php.
2.4 Route-Konfiguration aktualisieren
# config/routes.yaml
_pimcore:
resource: "@PimcoreCoreBundle/config/routing.yaml" # war: Resources/config/routing.yml
# config/routes/dev/routes.yaml
_pimcore:
resource: "@PimcoreCoreBundle/config/routing_dev.yaml" # war: Resources/config/routing_dev.yml
2.5 Messenger Transport Konfiguration
Pimcore 11 führt neue asynchrone Message-Transports ein:
framework:
messenger:
transports:
# Deine bestehenden Transports...
# NEU für 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"
Bei RabbitMQ (AMQP) erstellt jeder Transport eine separate Queue. Bei Doctrine Transport erstellt jeder eine separate Tabellenpartition. Plane dein Worker-Deployment entsprechend.
2.6 Migrations ausführen und Rebuild
bin/console doctrine:migrations:migrate
bin/console pimcore:cache:clear
bin/console cache:clear
rm -rf var/cache/*
Teste alles an diesem Checkpoint bevor du weitermachst.
Falls du während dieses Upgrades auch Workflows redesignst, lies unseren Guide zum Pimcore Enterprise Workflow Design für die Architekturmuster.
Phase 3: Upgrade auf Pimcore 12
3.1 Lizenz und Produktregistrierung
Pimcore 12 führt zwei verpflichtende Änderungen ein:
Lizenz: POCL (Pimcore Open Core License) ersetzt GPLv3. Community Edition mit Admin UI Classic Bundle erfordert eine ExtJS-Lizenz (ca. EUR 1.480).
| Edition | Preis |
|---|---|
| Community (POCL) | Kostenlos (Umsatz unter EUR 5 Mio.) |
| Professional | EUR 8.400/Jahr |
| Enterprise Self-Hosted | EUR 25.200/Jahr |
| Enterprise PaaS | Ab USD 39.900/Jahr |
Produktregistrierung: Verpflichtend. Drei Umgebungsvariablen erforderlich:
| Variable | Zweck |
|---|---|
PIMCORE_ENCRYPTION_SECRET | Defuse-Verschlüsselungsschlüssel, einmal pro Installation generiert |
PIMCORE_PRODUCT_KEY | Registrierungsschlüssel von license.pimcore.com |
PIMCORE_ENTERPRISE_TOKEN | Composer-Auth-Token für private Packages |
Registrierungsschritte:
# 1. Verschlüsselungssecret generieren (einmal pro Installation, NIEMALS danach ändern)
vendor/bin/generate-defuse-key
# 2. Registrierungs-Hash berechnen
php -r "echo hash_hmac('sha256', 'your-instance-id', 'your-encryption-secret');"
# 3. Registrierung unter license.pimcore.com mit dem Hash
# URL: https://license.pimcore.com/register?instance_identifier=ID&instance_hash=HASH
# 4. Product Key erhalten, als PIMCORE_PRODUCT_KEY Umgebungsvariable setzen
# config/config.yaml
pimcore:
encryption:
secret: '%env(PIMCORE_ENCRYPTION_SECRET)%'
product_registration:
product_key: '%env(PIMCORE_PRODUCT_KEY)%'
instance_identifier: 'your-instance-id'
Die Validierung ist komplett offline. Sie läuft bei jedem Symfony-Container-Build (Cache Clear, App-Start). Sie verifiziert die ECDSA-Signatur des Product Keys gegen einen mit Pimcore ausgelieferten Public Key. Kein Internet erforderlich. Wenn die Validierung fehlschlägt, wird InvalidConfigurationException geworfen und die App startet nicht.
Kritisch: Ändere den Verschlüsselungsschlüssel niemals nach dem initialen Setup. Alle verschlüsselten Daten in der Datenbank hängen davon ab. Behandle ihn wie ein Datenbankpasswort. Jede separate Installation (Staging, Production) kann denselben Schlüssel verwenden, wenn sie den gleichen Instance Identifier teilen.
3.2 Composer Update
composer require -W pimcore/pimcore:^12.0
3.3 Security-Konfiguration
security:
# ENTFERNEN (Standard in Symfony 6.4+):
# enable_authenticator_manager: true
firewalls:
pimcore_admin_webdav:
pattern: ^/asset/webdav # war: ^/admin/asset/webdav
access_control:
- { path: ^/asset/webdav, roles: ROLE_PIMCORE_USER }
3.4 WYSIWYG-Editor
# Option A: TinyMCE (Professional/Enterprise)
composer require pimcore/tinymce-bundle
# Option B: Quill (kostenlos, Community Edition)
composer require pimcore/quill-bundle
3.5 Doctrine DBAL 4 Abschluss-Änderungen
Neben den Methodenersetzungen aus Phase 1, aktualisiere die Doctrine-Konfiguration:
# config/packages/doctrine.yaml
doctrine:
dbal:
connections:
default:
driver: "pdo_mysql"
server_version: "mariadb-10.11" # Genau angeben
charset: utf8mb4
default_table_options:
charset: utf8mb4
collate: utf8mb4_unicode_520_ci # P12-Standard
Redis-Konfiguration im Detail
Redis erfüllt vier kritische Funktionen in einem Pimcore-12-Production-Deployment. Falsche Konfiguration verursacht stille Fehler, die schwer zu diagnostizieren sind.
1. Application Cache (Tag-Aware)
# config/packages/cache.yaml
framework:
cache:
# Pimcore 10 (ALT)
# pools:
# pimcore.cache.pool:
# tags: true # ENTFERNEN
# adapter: pimcore.cache.adapter.redis_tag_aware # ALTER Adapter-Name
# Pimcore 11+/12 (NEU)
pools:
pimcore.cache.pool:
public: true
default_lifetime: 31536000 # 1 Jahr
adapter: cache.adapter.redis_tag_aware # Geänderter Adapter-Name
provider: 'redis://%env(REDIS_HOST)%:%env(REDIS_PORT)%'
Die Änderung des Adapter-Namens von pimcore.cache.adapter.redis_tag_aware zu cache.adapter.redis_tag_aware ist still. Der alte Name wirft keinen Fehler. Das Caching fällt einfach still auf Filesystem zurück, und du fragst dich, warum dein Multi-Pod-Deployment überall veraltete Daten hat.
Ohne Redis-Caching trifft jeder Page-Render die Datenbank für Navigationsbäume, Site-Settings, Klassendefinitionen, Übersetzungen und mehr. Bei einem Multi-Pod-Kubernetes-Deployment pflegt jeder Pod seinen eigenen Filesystem-Cache ohne Invalidierungs-Koordination. Redis macht die Cache-Invalidierung über alle Pods hinweg konsistent.
2. Session Storage
# config/packages/framework.yaml
framework:
session:
handler_id: '%env(REDIS_SESSION_DSN)%'
# Beispiel: redis://redis:6379/1
Für Multi-Pod-Deployments müssen Sessions geteilt werden. Ohne Redis-Sessions werden Benutzer ausgeloggt, wenn ihr Request einen anderen Pod trifft (Round-Robin Load Balancing). Das ist das sichtbarste Symptom einer fehlenden Redis-Konfiguration.
3. Full Page Cache
Pimcores Full Page Cache speichert gerenderten HTML-Output für anonyme Benutzer. In Pimcore 12 kann er Redis als Backend nutzen:
# config/packages/pimcore.yaml
pimcore:
full_page_cache:
enabled: true
lifetime: 7200 # 2 Stunden
exclude_patterns:
- '/admin'
- '/api'
exclude_cookie: 'pimcore_admin_sid'
Der Full Page Cache reduziert die Datenbanklast für öffentliche Seiten drastisch. Eine Seite, die 200ms zum Rendern aus der Datenbank braucht, wird in unter 5ms aus dem Cache ausgeliefert. Bei stark frequentierten Seiten ist das der Unterschied zwischen 2 Web-Pods und 10.
Cache-Invalidierung passiert automatisch, wenn Redakteure Inhalte veröffentlichen. Aber sei vorsichtig mit personalisiertem Content: Jede Seite, die je nach Benutzerkontext variiert (Login-Status, Locale, A/B-Test-Variante), muss vom Full Page Caching ausgeschlossen oder mit ESI-Fragments versehen werden.
4. Messenger Transport (optional)
framework:
messenger:
transports:
pimcore_core:
dsn: 'redis://%env(REDIS_HOST)%:%env(REDIS_PORT)%/messages'
Redis Streams können RabbitMQ als Message Transport ersetzen. Einfacher zu betreiben (ein Service weniger), aber ohne Dead-Letter-Queues, Message-Prioritäten und Exchange-Routing. Für Installationen mit komplexen Worker-Topologien bleibt RabbitMQ besser. Für einfachere Setups funktioniert Redis Streams einwandfrei.
Redis Sizing
| Pimcore-Grösse | Redis Memory | Hinweise |
|---|---|---|
| Klein (< 10K Objekte) | 256 MB | Einzelinstanz |
| Mittel (10K-100K Objekte) | 1-2 GB | Einzelinstanz, Persistence aktiviert |
| Gross (100K+ Objekte) | 4-8 GB | Redis Sentinel oder Cluster erwägen |
Überwache used_memory und evicted_keys. Wenn Keys evicted werden, ist dein Cache zu klein und du verlierst Performance.
Die Collation-Migration (Der schwierigste Teil)
Dieser Abschnitt spart die meiste Zeit. Die Collation-Migration ist der gefährlichste und am wenigsten dokumentierte Teil des gesamten Upgrades.
Warum es bricht
Pimcore-12-Migrations erstellen neue Tabellen und ändern bestehende Spalten mit der Collation utf8mb4_unicode_520_ci. Deine bestehende Datenbank von Pimcore 10 verwendet den MySQL-Server-Standard. Bei Azure MySQL 8.0 ist das utf8mb4_0900_ai_ci. Bei älteren Setups vielleicht utf8mb4_general_ci.
Das Ergebnis: gemischte Collations innerhalb derselben Tabelle und tabellenübergreifend. Wenn Pimcore UNION-Queries über objects-, assets- und documents-Tabellen ausführt:
-- Das ist, was Pimcores getRelationData() ausführt
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 ...
Wenn objects.className utf8mb4_unicode_520_ci ist (aus der P12-Migration) aber assets.type utf8mb4_0900_ai_ci (aus dem Azure-Standard), wirft MySQL:
SQLSTATE[HY000]: General error: 1267 Illegal mix of collations for operation 'UNION'
Dieser Fehler erscheint, wenn man ein beliebiges DataObject in der Admin UI öffnet. Das System ist effektiv kaputt.
Tatsächliche Verteilung, die du finden wirst
Bei einer Datenbank, die von P10 über P11 nach P12 auf Azure MySQL 8.0 migriert wurde:
| Collation | Tabellen | Spalten | Quelle |
|---|---|---|---|
utf8mb4_0900_ai_ci | ~580 | ~5.700 | Azure MySQL 8.0 Standard |
utf8mb4_unicode_ci | ~87 | ~155 | Alte Bundle-Tabellen |
utf8mb4_unicode_520_ci | ~6 | ~994 | Durch P12-Migration erstellt |
utf8mb3_general_ci | ~4 | ~31 | Sehr alte Legacy-Tabellen |
utf8mb4_general_ci | ~1 | ~3 | Bestimmte Bundle-Tabellen |
# Aktuelle Verteilung prüfen
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"
# Spaltenebene prüfen (hier versteckt sich das eigentliche Problem)
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"
Fünf Spezialfälle, die einfaches ALTER TABLE brechen
1. Tabellen mit Path-Index (assets, documents, objects, http_error_log):
Composite-Indexes auf path + key/filename überschreiten das 3072-Byte-InnoDB-Index-Limit nach der Collation-Konvertierung (utf8mb4 = 4 Bytes/Zeichen, 255 Zeichen x 4 = 1020 Bytes pro Spalte, Composite = über dem Limit).
-- Indexes droppen, konvertieren, mit Prefix-Längen neu erstellen
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. Classification Store Tabellen: FK-Constraint zwischen object_classificationstore_data_* und object_classificationstore_groups_*. Der FK muss vor der Konvertierung gedroppt und danach neu erstellt werden. Der Spaltenname kann id (P12) oder o_id (P10/11) sein, je nach Migrationsstatus.
3. Übersetzungstabellen (translations_admin, translations_messages): Die Collation-Änderung kann case-insensitive Duplikate aufdecken. Wenn du sowohl "MyKey" als auch "mykey" als separate Übersetzungsschlüssel hast (gültig unter case-sensitiver Collation), verursacht die Konvertierung zu case-insensitiver Collation eine Unique-Constraint-Violation. Wir haben ca. 1.600 Duplikate in einer einzigen Staging-Datenbank gefunden. Duplikate müssen vor der Konvertierung entfernt werden.
4. Assets-Tabelle: Der fullpath-Index muss als non-unique neu erstellt werden. Dateinamen mit unterschiedlicher Gross-/Kleinschreibung können existieren (z.B. EEVA.tif und Eeva.tif sind legitim verschiedene Dateien). Ein Unique-Index würde die zweite nach der Collation-Änderung ablehnen.
5. Grosse Tabellen: Tabellen mit Millionen von Zeilen (lokalisierte Query-Tabellen haben z.B. 30+ Tabellen pro Klasse mit einer pro Locale) brauchen erhebliche Zeit für ALTER. Bei einer Datenbank mit 680 Tabellen dauert die vollständige Konvertierung ca. 18 Minuten.
Migration durchführen
Führe sie während eines Wartungsfensters mit herunterskalierten Web-Pods aus:
# Herunterskalieren, um Lock-Contention auf Übersetzungstabellen zu vermeiden
kubectl scale deployment pimcore pimcore-frontend -n <namespace> --replicas=0
kubectl get pods -n <namespace> -l app=pimcore -w # Auf Terminierung warten
Nach dem Ausführen des automatisierten Migrationsskripts (das alle fünf Spezialfälle behandelt):
# Klassendefinitionen rebuilden (stellt FK-Constraints wieder her)
PIMCORE_CLASS_DEFINITION_WRITABLE=1 bin/console pimcore:deployment:classes-rebuild -c
# Verifizieren, dass nur noch eine Collation übrig ist
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"
# Wieder hochskalieren
kubectl scale deployment pimcore -n <namespace> --replicas=1
kubectl scale deployment pimcore-frontend -n <namespace> --replicas=3
Kurzreferenz: Collation-Fehler
| Fehler | Ursache | Lösung |
|---|---|---|
Illegal mix of collations for operation 'UNION' | Gemischte Collations über Tabellen hinweg in Relation-Queries | Alle Tabellen auf utf8mb4_unicode_520_ci konvertieren |
Specified key was too long; max key length is 3072 bytes | Composite path+key Index überschreitet Limit | Index droppen, konvertieren, mit Prefix-Längen neu erstellen |
Referencing column and referenced column are incompatible | FK zwischen Tabellen mit unterschiedlichen Collations | FK droppen, beide Tabellen konvertieren, Classes rebuilden |
Duplicate entry for key 'PRIMARY' | Collation-Änderung macht case-unterschiedliche Schlüssel identisch | Duplikate vor der Konvertierung entfernen |
Lock wait timeout exceeded | App hält Locks auf Übersetzungstabellen | Pods vor ALTER herunterskalieren |
Datenbankoptimierung nach der Migration
Nach der Collation-Migration, OPTIMIZE auf allen Tabellen ausführen um den Speicher zu defragmentieren und Indexes neu aufzubauen:
# Core-Tabellen
bin/console doctrine:query:sql "OPTIMIZE TABLE assets, documents, objects, versions, translations_admin, translations_messages, search_backend_data, properties, dependencies"
# Auf hohe Fragmentierung prüfen
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"
MySQL SSL-Konfiguration
Für verwaltete MySQL-Services (Azure, AWS RDS) mit 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 aktiviert SSL ohne Angabe eines Zertifikatspfads. Der Server stellt sein Zertifikat bereit und der Client vertraut ihm. MYSQL_ATTR_SSL_VERIFY_SERVER_CERT: false überspringt die Hostname-Verifizierung (sicher für internes Networking).
Wie wir Datenbankmigrationen in unserer breiteren Data Engineering Praxis handhaben, beschreibt diese Seite unsere Methodik.
OpenSearch-Integration
Pimcore 12 führt das Generic Data Index Bundle ein, das OpenSearch (oder Elasticsearch) für Backend-Suche und Datenindexierung nutzt.
Index-Setup (muss vor Migrations passieren)
# Leere Indices manuell erstellen (erforderlich bevor Migrations laufen)
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 '{}'
# Portal-Engine-Statistik-Indices NICHT manuell erstellen
# Sie werden automatisch mit zeitbasierten Namen erstellt: {name}__{year}_{month}
# Manuelle Erstellung kollidiert mit dem Alias-Muster
Das ist kritisch, weil einige P12-Migrations Document->save() triggern, was das DocumentIndexUpdateSubscriber-Event auslöst. Wenn der Index nicht existiert: index_not_found_exception gefolgt von kaskadierenden SAVEPOINT-Fehlern, die den Migrationsstatus korrumpieren.
Nach Abschluss der Migrations:
# Indices für alle DataObject-Klassen erstellen
php -d memory_limit=2G bin/console generic-data-index:deployment:reindex --no-interaction
# Verfügbare Befehle
bin/console list generic-data-index
# generic-data-index:deployment:reindex Erstellt/aktualisiert Indices für classDefinitions
# generic-data-index:reindex Triggert native Reindexierung
# generic-data-index:update:index Aktualisiert Index/Mapping
OpenSearch Cluster Sizing
| Installationsgrösse | Nodes | Memory/Node | Storage/Node | Java Heap |
|---|---|---|---|---|
| Klein (< 10K Objekte) | 1 | 4 GB | 50 GB SSD | 2 GB |
| Mittel (10K-100K) | 2 | 8 GB | 100 GB SSD | 4 GB |
| Gross (100K+) | 3 (master-eligible) | 16 GB | 200 GB SSD | 8 GB |
Falls du Elasticsearch 7.x ersetzt, ist OpenSearch ein Drop-in-Replacement. Die API ist kompatibel. Nur die Client-Library und Konfigurationspfade ändern sich. Wie wir Sucharchitektur breiter angehen, beschreibt unser E-Commerce-Plattformen-Guide.
OpenSearch Security
Für interne Cluster-Kommunikation (Pods im selben Kubernetes-Namespace) kann das Security-Plugin deaktiviert werden:
# opensearch-cluster.yaml
additionalConfig:
plugins.security.disabled: "true"
Für externen Zugang oder Multi-Tenant-Cluster konfiguriere eine richtige Authentifizierung.
Flysystem und Remote-Storage-Bugs
Falls du Remote-Asset-Storage nutzt (Azure Blob, AWS S3, Google Cloud Storage), hat Pimcore 12 zwei Bugs, die du kennen musst.
Bug 1: getDimensions() lädt bei jedem Page-Render jedes Bild herunter
Betrifft: Pimcore 10.6+, 11.x, 12.x mit Remote-Flysystem-Storage
Jeder thumbnail()->html()-Aufruf triggert getDimensions(), was readDimensionsFromFile() aufruft, bevor getEstimatedDimensions() versucht wird. Bei Remote-Storage kostet jeder Dateizugriff 50-100ms an Netzwerk-I/O.
Gemessene Auswirkung: Eine Seite mit ca. 80 Bildreferenzen brauchte 5.735ms (79 Remote-Storage-Aufrufe mit je ca. 65ms). Nach der Korrektur der Methodenreihenfolge renderte dieselbe Seite in 170ms mit null Remote-Aufrufen.
Ursache in ImageThumbnailTrait::getDimensions():
// Aktuelle Reihenfolge (FALSCH für Remote Storage)
// Schritt 1: DB-Thumbnail-Cache prüfen (schnell, aber oft ein Miss)
// Schritt 2: readDimensionsFromFile() → LÄDT DATEI AUS CLOUD HERUNTER (~65ms)
// Schritt 3: getEstimatedDimensions() → WIRD NIE ERREICHT (Schritt 2 ist immer erfolgreich)
// Korrigierte Reihenfolge
if (empty($dimensions) && $config && $asset instanceof Image) {
$dimensions = $config->getEstimatedDimensions($asset); // ~0ms, reine Mathematik
}
if (empty($dimensions) && $this->exists()) {
$dimensions = $this->readDimensionsFromFile(); // 50-100ms, nur als letzter Ausweg
}
getEstimatedDimensions() berechnet Dimensionen aus den Originalbildmassen (in der Datenbank gespeichert) und der Thumbnail-Transformationskonfiguration. Reine Mathematik, null I/O. In Production haben 95%+ der Bilder gespeicherte Dimensionen. Dieser Fix eliminiert praktisch alle Remote-Storage-I/O während des Page-Renderings.
Bug 2: Ordnervorschau-Thumbnails hängen beim Ladebalken
Betrifft: admin-ui-classic-bundle 2.2.x, 2.3.x
Nach dem Hochladen von Bildern zeigt der Preview-Tab einen Ladebalken, der sich nie auflöst. Der Browser cached das Platzhalter-GIF permanent, weil die URL modificationDate als Cache-Buster nutzt (ändert sich nie), und keine asynchrone Thumbnail-Generierung für den folderPreview-Ursprung dispatcht wird (im Gegensatz zu treeNode, der das tut).
Lösung: Cache-Control: no-store zur Platzhalter-Response hinzufügen und AssetPreviewImageMessage dispatchen.
Kubernetes-Deployment
Pod-Architektur
| Pod | Zweck | Replicas |
|---|---|---|
pimcore-web | Nginx + PHP-FPM, Admin + Frontend | 2-4 |
pimcore-worker | Symfony Messenger Consumers | 1-3 |
pimcore-ops | Wartungs-CLI-Pod (Migrations, Rebuilds) | 1 |
redis | Cache, Sessions, optionaler Transport | 1+ |
mysql | Datenbank (oder Managed Service) | 1+ |
opensearch | Suchindices | 2-3 |
rabbitmq | Message Broker (falls nicht Redis) | 1-3 |
Secrets Management
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>
Verwende Sealed Secrets (Bitnami) oder externe Secret-Operatoren für Production. Committe niemals Klartext-Secrets nach Git.
Jeder Init-Container (create-assets, create-classes, db-migrations, clear-cache) muss ALLE secretRefs in seiner envFrom-Liste haben. Fehlende Secrets verursachen stille Environment variable not found-Fehler.
Worker-Deployment
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 und --memory-limit stellen sicher, dass Worker periodisch neustarten und PHP-Memory-Leaks verhindern. Kubernetes startet den Pod neu, wenn der Prozess sauber beendet wird.
Deployment-Reihenfolge
1. Kubernetes-Ressourcen deployen
2. Shell in den Ops-Pod öffnen
3. Fehlende Bundles installieren (PimcoreAdminBundle, GenericExecutionEngine, etc.)
4. OpenSearch-Indices manuell erstellen (pimcore_document, pimcore_asset)
5. OpenSearch DataObject Reindex ausführen
6. Datenbank-Migrations ausführen (mit PIMCORE_CLASS_DEFINITION_WRITABLE=1)
7. Collation-Migration ausführen (Wartungsfenster, Web-Pods herunterskaliert)
8. Datenbankoptimierung ausführen (OPTIMIZE TABLE)
9. Post-Deploy-Befehle (classes-rebuild, reindex, assets:install, cache:clear)
10. Verifizieren: DataObject im Admin öffnen, Assets prüfen, Frontend prüfen
Unsere Cloud-Services decken Kubernetes-Deployment und -Betrieb für Pimcore und andere Plattformen ab.
Event-System-Architektur in Pimcore 12
Das Verständnis von Pimcores Event-System ist sowohl für das Upgrade als auch für das Bauen von Extensions kritisch.
PHP Events (Backend)
Pimcore nutzt Symfonys EventDispatcher. Wichtige Events für DataObjects:
use Pimcore\Event\DataObjectEvents;
// Verfügbare Events
DataObjectEvents::PRE_ADD // Vor dem ersten Speichern
DataObjectEvents::POST_ADD // Nach dem ersten Speichern
DataObjectEvents::PRE_UPDATE // Vor jedem Speichern
DataObjectEvents::POST_UPDATE // Nach jedem Speichern
DataObjectEvents::PRE_DELETE // Vor dem Löschen
DataObjectEvents::POST_DELETE // Nach dem Löschen
Einen Event Subscriber in Pimcore 12 erstellen:
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;
}
// In Suchindex eintragen, Assets generieren, etc.
}
}
JavaScript Events (Admin UI)
Pimcore 12 nutzt ein anderes Event-Subscription-Pattern als P10:
// Pimcore 12 Admin Events
document.addEventListener(pimcore.events.preOpenObject, (e) => {
// Bevor der Object-Editor öffnet
});
document.addEventListener(pimcore.events.postOpenObject, (e) => {
// Nachdem der Object-Editor öffnet, e.detail.object verfügbar
});
document.addEventListener(pimcore.events.preSaveObject, (e) => {
// Vor dem Speichern, kann mit e.preventDefault() abgebrochen werden
});
document.addEventListener(pimcore.events.postSaveObject, (e) => {
// Nachdem das Speichern abgeschlossen ist
});
Pimcore 12 erweitern
Custom Bundles in Pimcore 12 folgen Symfony-Bundle-Konventionen:
// 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',
];
}
}
Custom Admin JS und CSS in der Bundle-Konfiguration registrieren:
# 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'
Post-Upgrade-Verifizierung
Befehlssequenz
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
Funktionale Testmatrix
| Bereich | Was testen | Wie verifizieren |
|---|---|---|
| Admin-Login | Login, 2FA, Logout | Manuell im Browser |
| DataObjects | Erstellen, bearbeiten, speichern, veröffentlichen, löschen, Versionen | Beliebiges Objekt öffnen, Feld bearbeiten, speichern |
| Assets | Upload, Thumbnails, Metadaten, Download | Bild hochladen, Thumbnail-Generierung prüfen |
| Dokumente | Seiten bearbeiten, Areabricks, speichern, veröffentlichen | Seite mit Areabricks bearbeiten |
| Relationen | Objekt-Relationen, Asset-Relationen | Objekt mit Relationen öffnen, UNION-Query verifizieren |
| Suche | Backend-Suche, OpenSearch-Indexierung | Nach Objekt in der Admin-Toolbar suchen |
| Messenger | Queue-Verarbeitung, alle Transports | bin/console messenger:stats prüfen |
| Cache | Redis-Konnektivität, Invalidierung | Content veröffentlichen, Änderung auf anderem Pod verifizieren |
| Full Page Cache | Anonymes Seiten-Caching, Invalidierung | Seite laden, Response-Headers auf Cache-Hit prüfen |
| ERP-Integration | Import-Verarbeitung, Feld-Mapping | Testimport ausführen |
| Berechtigungen | RBAC, Workflow-Berechtigungen | Als eingeschränkter Benutzer einloggen, Zugriff verifizieren |
| Custom JS | Grid Operators, Admin-Plugins | Grid-View öffnen, Custom-Spalten prüfen |
Performance-Benchmarks
| Metrik | Akzeptabel | Warnung | Kritisch |
|---|---|---|---|
| Admin-Seitenload | < 2s | 2-5s | > 5s |
| DataObject öffnen | < 3s | 3-8s | > 8s |
| Asset-Upload (10MB) | < 5s | 5-15s | > 15s |
| Suchantwort | < 500ms | 500ms-2s | > 2s |
| Frontend-Seite (gecacht) | < 50ms | 50-200ms | > 200ms |
| Frontend-Seite (ungecacht) | < 500ms | 500ms-2s | > 2s |
| Thumbnail-Render (Remote Storage) | < 200ms | 200ms-1s | > 1s (getDimensions-Bug prüfen) |
Häufige Stolperfallen
-
Pimcore 11 überspringen. Du kannst nicht von 10 auf 12 springen. Die Zwischenversion verarbeitet kritische Schema-Änderungen, Bundle-Extraktion und Prefix-Entfernung.
-
Migrations ohne OpenSearch-Indices ausführen. Einige Migrations triggern Event Subscribers, die OpenSearch abfragen.
index_not_found_exceptionkaskadiert in einen korrupten SAVEPOINT-Status. Indices zuerst erstellen. -
Collations nicht fixen. Gemischte Collations verursachen UNION-Query-Fehler beim Öffnen jedes DataObjects. Das bricht die Admin UI still, bis ein Redakteur ein Produkt öffnet.
-
Den Verschlüsselungsschlüssel verlieren. Falls nach dem Deployment verloren, werden alle verschlüsselten Daten in der Datenbank unwiederbringlich. Das ist nicht wiederherstellbar. Behandle ihn wie das Master-Datenbankpasswort.
-
Stiller Redis-Cache-Fallback. Die Adapter-Namensänderung von
pimcore.cache.adapter.redis_tag_awarezucache.adapter.redis_tag_awarewirft keine Fehler. Caching fällt still auf Filesystem zurück. Multi-Pod-Deployments brechen. -
Messenger Transports nicht aktualisieren. Fehlende P11-Transports bewirken, dass Scheduled Tasks, Asset-Verarbeitung und Suchindexierung still aufhören zu funktionieren.
-
Collation-Migration mit laufenden Web-Pods ausführen. Die Konvertierung der Übersetzungstabellen erzeugt Deadlocks bei gleichzeitigem Zugriff. Zuerst herunterskalieren.
-
Portal-Engine-Statistik-Indices manuell erstellen. Die automatische Erstellung nutzt zeitbasierte Index-Namen mit Aliases. Manuelle Erstellung kollidiert mit dem Alias-Muster.
-
Auf leerer Datenbank testen. Schema-Migration auf einer leeren Datenbank verbirgt 90% der Probleme. Verwende einen vollständigen Production-Datenklon.
-
Den getDimensions()-Bug bei Cloud-Storage ignorieren. Jeder ungecachte Page-Render ist 3-7 Sekunden langsamer als nötig, bis das gepatcht ist. Der Fix ist eine Zwei-Zeilen-Umstellung.
-
Fehlende
getSupportedTypes()bei Normalizern. Symfony 7.x erfordert diese Methode. Fehlt sie, kommt ein Fatal Error, keine Deprecation-Warnung. -
Request::get()-Aufrufe nicht aktualisieren. Deprecated, funktioniert aber noch in Symfony 6.x. Wird in 7.x brechen. -
PIMCORE_CLASS_DEFINITION_WRITABLE=1vergessen. Migrations und Class Rebuilds scheitern still ohne diese Umgebungsvariable. Die Fehlermeldung ist klar, aber es wird leicht vergessen. -
ExtJS 7
rootvsrootProperty. Custom Store Readers mitroot: 'data'parsen Responses in ExtJS 7 still falsch.
Zentrale Erkenntnisse
-
Drei Phasen, keine Abkürzungen. Vorbereitung (veralteten Code fixen), Pimcore 11 (Framework-Upgrade + Bundle-Extraktion), Pimcore 12 (Registrierung, DBAL 4, Collation, Event-System). Jede Phase ist ein Checkpoint.
-
Collation-Migration ist der schwierigste Teil. Fünf Typen von Spezialtabellen brauchen Custom-Handling. Plane ein Wartungsfenster. Nutze ein automatisiertes Skript. Verifiziere auf Tabellen- und Spaltenebene.
-
Produktregistrierung ist verpflichtend und offline. Kein Internet nötig für die Validierung. Aber der Verschlüsselungsschlüssel ist unwiederbringlich, wenn verloren.
-
Redis ist in Multi-Pod-Deployments nicht optional. Cache-Konsistenz, Session-Sharing, Full Page Cache und optional Message Transport hängen alle davon ab. Die Adapter-Namensänderung ist ein stiller Breaking Change.
-
OpenSearch-Indices müssen vor Migrations existieren. Document- und Asset-Indices manuell erstellen, dann DataObject-Reindex ausführen. Die Reihenfolge ist wichtig.
-
Die Event-System-Migration ist mechanisch aber kritisch. Plugin Broker zu Event Listeners,
o_classNamezuclassName,@Routezu#[Route],Request::get()zu spezifischen Accessoren. Rector kann 80% davon automatisieren. -
Remote Storage deckt Pimcore-Bugs auf. Der getDimensions()-Reihenfolge-Bug fügt jeder Seitenrendering Sekunden hinzu. Der Ordnervorschau-Cache-Bug macht Thumbnails permanent hängen. Beide haben einfache Fixes.
-
Auf einem Production-Datenklon testen. Jedes Mal. Ein Upgrade auf leerer Datenbank dauert 20 Minuten und gelingt trivial. Echte Probleme treten auf bei 70K Assets, 680 Tabellen mit gemischten Collations und 50K+ Datenobjekten.
Das ist genau die Art von Software Engineering Arbeit, die unser Team regelmässig macht. Wenn du ein Pimcore-Upgrade planst, hat unser Consulting-Team das für mehrere Enterprise-Installationen in der DACH-Region durchgeführt. Wir bieten es auch als Teil unserer breiteren Services an.
Bereit, dein Pimcore-Upgrade zu starten? Sprich mit unserem Team oder fordere ein Angebot an.
Behandelte Themen
Verwandte Guides
Unternehmenshandbuch zu Agentischen KI-Systemen
Technischer Leitfaden zu agentischen KI-Systemen in Unternehmen. Erfahre mehr ueber Architektur, Faehigkeiten und Anwendungen autonomer KI-Agenten.
Guide lesenAgentic Commerce: Wie du KI-Agenten sicher einkaufen lässt
Wie du gesteuerten, KI-initiierten Handel designst. Policy Engines, HITL-Freigabe-Gates, HMAC-Quittungen, Idempotenz, Tenant-Scoping und das vollständige Agentic Checkout Protocol.
Guide lesenDie 9 Stellen, an denen dein KI-System Daten verliert (und wie du jede einzelne abdichtest)
Eine systematische Übersicht aller Stellen, an denen KI-Systeme Daten preisgeben. Prompts, Embeddings, Logs, Tool Calls, Agent Memory, Fehlermeldungen, Cache, Fine-Tuning-Daten und Agent Handoffs.
Guide lesenBereit, produktionsreife KI-Systeme zu bauen?
Unser Team ist spezialisiert auf produktionsreife KI-Systeme. Lass uns besprechen, wie wir deinem Unternehmen helfen können.
Gespräch starten