Technischer Leitfaden

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.

20. März 202645 Min. LesezeitOronts Engineering Team

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:

KomponentePimcore 10.6Pimcore 11Pimcore 12
PHP8.0+8.1+8.3+
Symfony5.46.2+6.4 / 7.x
Doctrine DBAL2.x / 3.x3.x4.x
Admin UIBuilt-in (ExtJS 6)Separates Bundle (ExtJS 6/7)Separates Bundle (ExtJS 7) + Studio UI (React)
LizenzGPLv3GPLv3POCL (Pimcore Open Core License)
Core BundlesMonolithischExtrahiertExtrahiert
Such-BackendBuilt-inSimple Backend Search BundleGeneric Data Index + OpenSearch
WYSIWYGTinyMCE built-inTinyMCE BundleTinyMCE oder Quill Bundle
ProduktregistrierungKeineKeineErforderlich (Offline-Validierung)
Event-System (JS)Plugin BrokerEvent ListenersEvent Listeners + Studio UI Events
Collationutf8mb4_general_ciutf8mb4_general_ciutf8mb4_unicode_520_ci (erforderlich)
CachePimcore AdapterSymfony AdapterSymfony Adapter (Konfigurationsänderung)
Full Page CacheBuilt-inBuilt-inSeparates Bundle (optional)
Asset-SpeicherLokal / FlysystemLokal / FlysystemLokal / 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 DELETEManuelles SQL
$db->updateWhere()$db->executeStatement() mit UPDATEManuelles SQL
$db->quote($val, $type)$db->quote($val)Nur Strings in DBAL 4, kein Type-Parameter
Pimcore\Db\ConnectionInterfaceDoctrine\DBAL\ConnectionType-Hint-Änderung in DI
Pimcore\Db\ConnectionDoctrine\DBAL\ConnectionKlassen-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_idid (oder oo_id in Object-Store-Tabellen)Primärschlüssel
o_creationDatecreationDateZeitstempel
o_modificationDatemodificationDateZeitstempel
o_pathpathBaumpfad
o_keykeyObject-Key/Slug
o_publishedpublishedVeröffentlichungs-Flag
o_parentIdparentIdParent-Referenz
o_typetypeObjekttyp
o_classNameclassNameKlassenname
o_classIdclassIdKlassen-ID
o_userOwneruserOwnerErsteller-User-ID
o_userModificationuserModificationLetzte Bearbeiter-User-ID
o_versionCountversionCountVersionszä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 entfernenCSRF wird anders behandelt
o_className in JSclassNameEntspricht der PHP-Änderung
Class.create(...)Weiterhin unterstützt, aber ES6-Klassen bevorzugenZukunftssicher
/admin/tags/*Kann sich zu /pimcore-admin/tags/* ändernAdmin-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 zu rootProperty in 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).

EditionPreis
Community (POCL)Kostenlos (Umsatz unter EUR 5 Mio.)
ProfessionalEUR 8.400/Jahr
Enterprise Self-HostedEUR 25.200/Jahr
Enterprise PaaSAb USD 39.900/Jahr

Produktregistrierung: Verpflichtend. Drei Umgebungsvariablen erforderlich:

VariableZweck
PIMCORE_ENCRYPTION_SECRETDefuse-Verschlüsselungsschlüssel, einmal pro Installation generiert
PIMCORE_PRODUCT_KEYRegistrierungsschlüssel von license.pimcore.com
PIMCORE_ENTERPRISE_TOKENComposer-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össeRedis MemoryHinweise
Klein (< 10K Objekte)256 MBEinzelinstanz
Mittel (10K-100K Objekte)1-2 GBEinzelinstanz, Persistence aktiviert
Gross (100K+ Objekte)4-8 GBRedis 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:

CollationTabellenSpaltenQuelle
utf8mb4_0900_ai_ci~580~5.700Azure MySQL 8.0 Standard
utf8mb4_unicode_ci~87~155Alte Bundle-Tabellen
utf8mb4_unicode_520_ci~6~994Durch P12-Migration erstellt
utf8mb3_general_ci~4~31Sehr alte Legacy-Tabellen
utf8mb4_general_ci~1~3Bestimmte 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

FehlerUrsacheLösung
Illegal mix of collations for operation 'UNION'Gemischte Collations über Tabellen hinweg in Relation-QueriesAlle Tabellen auf utf8mb4_unicode_520_ci konvertieren
Specified key was too long; max key length is 3072 bytesComposite path+key Index überschreitet LimitIndex droppen, konvertieren, mit Prefix-Längen neu erstellen
Referencing column and referenced column are incompatibleFK zwischen Tabellen mit unterschiedlichen CollationsFK droppen, beide Tabellen konvertieren, Classes rebuilden
Duplicate entry for key 'PRIMARY'Collation-Änderung macht case-unterschiedliche Schlüssel identischDuplikate vor der Konvertierung entfernen
Lock wait timeout exceededApp hält Locks auf ÜbersetzungstabellenPods 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össeNodesMemory/NodeStorage/NodeJava Heap
Klein (< 10K Objekte)14 GB50 GB SSD2 GB
Mittel (10K-100K)28 GB100 GB SSD4 GB
Gross (100K+)3 (master-eligible)16 GB200 GB SSD8 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

PodZweckReplicas
pimcore-webNginx + PHP-FPM, Admin + Frontend2-4
pimcore-workerSymfony Messenger Consumers1-3
pimcore-opsWartungs-CLI-Pod (Migrations, Rebuilds)1
redisCache, Sessions, optionaler Transport1+
mysqlDatenbank (oder Managed Service)1+
opensearchSuchindices2-3
rabbitmqMessage 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

BereichWas testenWie verifizieren
Admin-LoginLogin, 2FA, LogoutManuell im Browser
DataObjectsErstellen, bearbeiten, speichern, veröffentlichen, löschen, VersionenBeliebiges Objekt öffnen, Feld bearbeiten, speichern
AssetsUpload, Thumbnails, Metadaten, DownloadBild hochladen, Thumbnail-Generierung prüfen
DokumenteSeiten bearbeiten, Areabricks, speichern, veröffentlichenSeite mit Areabricks bearbeiten
RelationenObjekt-Relationen, Asset-RelationenObjekt mit Relationen öffnen, UNION-Query verifizieren
SucheBackend-Suche, OpenSearch-IndexierungNach Objekt in der Admin-Toolbar suchen
MessengerQueue-Verarbeitung, alle Transportsbin/console messenger:stats prüfen
CacheRedis-Konnektivität, InvalidierungContent veröffentlichen, Änderung auf anderem Pod verifizieren
Full Page CacheAnonymes Seiten-Caching, InvalidierungSeite laden, Response-Headers auf Cache-Hit prüfen
ERP-IntegrationImport-Verarbeitung, Feld-MappingTestimport ausführen
BerechtigungenRBAC, Workflow-BerechtigungenAls eingeschränkter Benutzer einloggen, Zugriff verifizieren
Custom JSGrid Operators, Admin-PluginsGrid-View öffnen, Custom-Spalten prüfen

Performance-Benchmarks

MetrikAkzeptabelWarnungKritisch
Admin-Seitenload< 2s2-5s> 5s
DataObject öffnen< 3s3-8s> 8s
Asset-Upload (10MB)< 5s5-15s> 15s
Suchantwort< 500ms500ms-2s> 2s
Frontend-Seite (gecacht)< 50ms50-200ms> 200ms
Frontend-Seite (ungecacht)< 500ms500ms-2s> 2s
Thumbnail-Render (Remote Storage)< 200ms200ms-1s> 1s (getDimensions-Bug prüfen)

Häufige Stolperfallen

  1. Pimcore 11 überspringen. Du kannst nicht von 10 auf 12 springen. Die Zwischenversion verarbeitet kritische Schema-Änderungen, Bundle-Extraktion und Prefix-Entfernung.

  2. Migrations ohne OpenSearch-Indices ausführen. Einige Migrations triggern Event Subscribers, die OpenSearch abfragen. index_not_found_exception kaskadiert in einen korrupten SAVEPOINT-Status. Indices zuerst erstellen.

  3. 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.

  4. 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.

  5. Stiller Redis-Cache-Fallback. Die Adapter-Namensänderung von pimcore.cache.adapter.redis_tag_aware zu cache.adapter.redis_tag_aware wirft keine Fehler. Caching fällt still auf Filesystem zurück. Multi-Pod-Deployments brechen.

  6. Messenger Transports nicht aktualisieren. Fehlende P11-Transports bewirken, dass Scheduled Tasks, Asset-Verarbeitung und Suchindexierung still aufhören zu funktionieren.

  7. Collation-Migration mit laufenden Web-Pods ausführen. Die Konvertierung der Übersetzungstabellen erzeugt Deadlocks bei gleichzeitigem Zugriff. Zuerst herunterskalieren.

  8. Portal-Engine-Statistik-Indices manuell erstellen. Die automatische Erstellung nutzt zeitbasierte Index-Namen mit Aliases. Manuelle Erstellung kollidiert mit dem Alias-Muster.

  9. Auf leerer Datenbank testen. Schema-Migration auf einer leeren Datenbank verbirgt 90% der Probleme. Verwende einen vollständigen Production-Datenklon.

  10. 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.

  11. Fehlende getSupportedTypes() bei Normalizern. Symfony 7.x erfordert diese Methode. Fehlt sie, kommt ein Fatal Error, keine Deprecation-Warnung.

  12. Request::get()-Aufrufe nicht aktualisieren. Deprecated, funktioniert aber noch in Symfony 6.x. Wird in 7.x brechen.

  13. PIMCORE_CLASS_DEFINITION_WRITABLE=1 vergessen. Migrations und Class Rebuilds scheitern still ohne diese Umgebungsvariable. Die Fehlermeldung ist klar, aber es wird leicht vergessen.

  14. ExtJS 7 root vs rootProperty. Custom Store Readers mit root: '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_className zu className, @Route zu #[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

Pimcore UpgradePimcore 12 MigrationPimcore 11 UpgradeSymfony 7 MigrationPHP 8.3 UpgradeDoctrine DBAL 4Pimcore KubernetesPimcore OpenSearchPimcore RedisPimcore Full Page CacheExtJS 7Pimcore Studio UIPimcore Event-System

Bereit, 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